├── VERSION ├── test ├── .gitignore └── test.butt ├── .gitignore ├── scripts ├── changelog-latest └── travis-update-release ├── configure ├── .editorconfig ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── .travis.yml ├── Makefile ├── CHANGELOG.md ├── flow.1.rst ├── README.md ├── LICENSE └── flow /VERSION: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | [0-9]* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | compiled -------------------------------------------------------------------------------- /scripts/changelog-latest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | BEFORE{out=0} 3 | /^## \[[0-9]/{if(out==0){out=1; next} else {out="end"}} out==1{print} 4 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for cmd in rst2man make; do 4 | type $cmd > /dev/null 2>&1 && continue 5 | echo "Command $cmd not found." >&2 6 | exit 1 7 | done 8 | echo "Configuration successful." -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request, workflow_dispatch] 3 | 4 | jobs: 5 | test: 6 | name: BUTT tests 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-python@v4 11 | - name: Install build requirements 12 | run: sudo apt-get install -y man docutils-common 13 | - name: Setup git user 14 | run: | 15 | git config --global user.email "github-actions@github.com" 16 | git config --global user.name "gh-actions" 17 | git config --global init.defaultBranch "master" 18 | - name: Build and install Flow 19 | run: | 20 | ./configure && make && sudo ./compiled/install 21 | - name: Install BUTT 22 | run: | 23 | curl -sL https://github.com/InternetGuru/butt/releases/download/v0.3.0/butt.sh > butt 24 | chmod +x butt 25 | - name: Run tests 26 | run: ./butt -vw. test/test.butt 27 | env: 28 | TERM: dumb 29 | -------------------------------------------------------------------------------- /scripts/travis-update-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ### 3 | # Travis-CI script to set GitHub release description 4 | # as part of deployment process. 5 | # 6 | # Expects: 7 | # - octokit to be installed; this is handled by Travis by installing dpl 8 | # - GITHUB_TOKEN environment variable with GitHub API secret 9 | # - TRAVIS_REPO_SLUG environment variable set to GitHub repo name (e.g. username/repo) 10 | # - TRAVIS_TAG environment variable set to Git tag which triggered the build 11 | # 12 | # Reads STDIN as a description of the release. 13 | ### 14 | require "octokit" 15 | 16 | gh_token = ENV.fetch("GITHUB_TOKEN") 17 | gh_slug = ENV.fetch("TRAVIS_REPO_SLUG") 18 | release_tag = ENV.fetch("TRAVIS_TAG") 19 | release_desc = STDIN.tty? ? '' : $stdin.read 20 | 21 | puts "Updating release details for #{gh_slug}, tag #{release_tag}" 22 | 23 | client = Octokit::Client.new(:access_token => gh_token) 24 | 25 | release = client.release_for_tag(gh_slug, release_tag) 26 | unless release 27 | puts "Release not found, creating draft" 28 | release = client.create_release(gh_slug, release_tag, {:draft => true}) 29 | end 30 | 31 | release_url = release.rels[:self].href 32 | puts "Release URL: #{release_url}" 33 | 34 | release_attributes = { 35 | :name => release_tag, 36 | :body => release_desc, 37 | :draft => false, 38 | :prerelease => false, 39 | } 40 | puts "Setting attributes:" 41 | p release_attributes 42 | 43 | client.update_release(release_url, release_attributes) 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install requirements 17 | run: sudo apt install -y python3-docutils 18 | 19 | - name: configure 20 | run: ./configure 21 | 22 | - name: Make dist 23 | run: make dist 24 | 25 | - name: Make distsingle 26 | run: make distsingle 27 | 28 | - name: Retrieve changes 29 | run: | 30 | EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) 31 | echo "JSON_RESPONSE<<$EOF" >> "$GITHUB_ENV" 32 | ./scripts/changelog-latest CHANGELOG.md >> "$GITHUB_ENV" 33 | curl https://example.com >> "$GITHUB_ENV" 34 | echo "$EOF" >> "$GITHUB_ENV" 35 | 36 | - name: Create Release 37 | id: create_release 38 | uses: actions/create-release@v1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | tag_name: ${{ github.ref }} 43 | release_name: Release ${{ github.ref }} 44 | body: ${{ env.CHANGES }} 45 | draft: false 46 | prerelease: false 47 | 48 | - name: Get Name of Artifact 49 | run: | 50 | echo "ARTIFACT_1_PATHNAME=$(ls *.tar.gz | head -n 1)" >> $GITHUB_ENV 51 | echo "ARTIFACT_2_PATHNAME=$(ls *.tar.gz | head -n 1)" >> $GITHUB_ENV 52 | 53 | - name: Upload Release Asset 54 | id: upload-release-asset 55 | uses: actions/upload-release-asset@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ steps.create_release.outputs.upload_url }} 60 | asset_path: ${{ env.ARTIFACT_1_PATHNAME }} 61 | asset_name: flow.tar.gz 62 | asset_content_type: application/gzip 63 | 64 | - name: Upload Second Release Asset 65 | id: upload-release-asset-2 66 | uses: actions/upload-release-asset@v1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: ${{ env.ARTIFACT_2_PATHNAME }} 72 | asset_name: flow.sh 73 | asset_content_type: application/x-sh 74 | 75 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | dist: trusty 8 | sudo: false 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - debian-sid # Needed for shellcheck 14 | packages: 15 | - man 16 | - shellcheck 17 | - python-docutils # Needed for rst2man 18 | 19 | env: 20 | global: 21 | - secure: "dFVeGQy4ngj5IgSyvZZpHn1r+AycBR6v8UMMQ/COkXfsPNuiTjAoO5VeVc2RibOT7TCs5w5pp5z/uRE8GBAif+Lrpt2KPkDr1eIoq6epgBi5LhF68wWLnLZVTNir3Tba0QFU4auO68PRbLQvjujkql3u8NIH6EStF03ypuwQzr8/jddTxW0sFqSBkC1O3EE5kgzY8GvW/UOI9PD+2ebEUBSCJBvpYqmpEnL9NOcUHvITWhJXT0q0qulJau1BbmMUT5uIHBnQ0A5HCcMJmRPfDOF/qYnhg4pKOjWsSOwA5itSKgdVxuAIabQ4L3FIKOGmw5mDGTsh1PpQwLD2Bfj0vjw/WhZNgXKzh5BRx/sBzh6tyZ9xfAd9eKWaMsG3shvJfPlCMGCgQGLc/+4fVRl/VNPyjFg1nEGa1wM9UwHc7J2wPn6GDQwmXCViKs/exvfkdJtm3CqnnE/4U+1jWAjbArmBWgDd/G71ytWgG3tPnrG9mQZfS+NgM61BxmMLkTh1zTYrdv6UoLrJXXW2atSkRPiV6GpcRegmW939RJ0Ymb4tosH6OrbE0vHAEtRKAzygNM9njRYkx4hO0tmPTaTpRDF/JCg7mu9OkgwfmK9L5ouJ3SfwxKnKZE/dY4KxJq8+asYJ2VNic70cz4Aq2X6/mZ4vDurPBv6/wdtre/9zc5g=" 22 | matrix: 23 | - DIST=regular DEPLOY_FILE=flow-*.tar.gz 24 | - DIST=single DEPLOY_FILE=flow.sh 25 | 26 | before_install: 27 | - | 28 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 29 | brew update 30 | brew tap homebrew/dupes 31 | brew install shellcheck docutils 32 | ln -s /usr/local/bin/rst2man.py /usr/local/bin/rst2man 33 | brew reinstall coreutils gnu-sed gnu-getopt gawk 34 | export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH" 35 | export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH" 36 | export PATH="/usr/local/opt/gnu-getopt/bin:$PATH" 37 | fi 38 | - export PREFIX=$HOME/opt 39 | - mkdir -p $PREFIX/bin 40 | - export PATH=$PREFIX/bin:$PATH 41 | 42 | install: 43 | # Install latest release of BUTT for Bash testing 44 | - curl -sL https://github.com/internetguru/butt/releases/download/v0.3.0/butt.sh > $PREFIX/bin/butt 45 | - chmod +x $PREFIX/bin/butt 46 | 47 | before_script: 48 | - git config --global user.email "travis@test.com" 49 | - git config --global user.name "travis" 50 | - | 51 | if [[ $DIST == regular ]]; then 52 | ./configure \ 53 | && make \ 54 | && cat compiled/install \ 55 | && compiled/install \ 56 | || return $? 57 | if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then PREFIX=/usr/local make dist; fi 58 | fi 59 | - | 60 | if [[ $DIST == single ]]; then 61 | ./configure \ 62 | && make distsingle \ 63 | && cp flow.sh $PREFIX/bin/flow 64 | fi 65 | 66 | script: 67 | - shellcheck flow 68 | - | 69 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 70 | butt -vs1 test/test.butt 71 | else 72 | butt -v test/test.butt 73 | fi 74 | 75 | deploy: 76 | - provider: releases 77 | api_key: "$GITHUB_TOKEN" 78 | file: "$DEPLOY_FILE" 79 | skip_cleanup: true 80 | file_glob: true 81 | on: 82 | repo: internetguru/flow 83 | tags: true 84 | condition: $TRAVIS_OS_NAME = linux 85 | - deploy: 86 | provider: script 87 | script: "scripts/changelog-latest CHANGELOG.md | scripts/travis-update-release" 88 | on: 89 | repo: internetguru/flow 90 | tags: true 91 | condition: $TRAVIS_OS_NAME = linux 92 | 93 | matrix: 94 | fast_finish: true 95 | 96 | notifications: 97 | slack: 98 | secure: qdgMIC+X9sgi0Y1ewzOjvFtxn7vCD7jCPAEqnsr3ruL7dT9dv5WaumJvNd3wpIWhAwjZwZ0FpCv50RB6QXPV4Mlc+RLwYI/CdOmBklifCOZIgJ+mvsJK4stZrodXxPc4nZ6vlAECIEkp3hQJOpqVSJfgoPVCi1AdKni3fXGGWVxPSUBkAN7JUs5bKXIMks0lYQ3yhzInvViy+o/ZTgIK9e35b+ykQqcIEdbpePbfXAEG8bE7z0m+r4Pp3yIS9QgX5eTvQOxMrtVK4KVFuzwj8A5KI37jfMDh09WohC60G1Fw1PpeDbb9FfXiWDU+ONsWZkW6r4VUMrrIBh6oWyHRMDeUkh12FQhcrmHX+adzOHbjFQcIGPYnBZmsKEZYPSf/NOzptWai4pgcDUg/t3kvXHpmCmtIwWxCfqR2bFjp3btXIDWycaHTvpOLyiy+Zsaj/5sEA6a1zfb1mmFn3rQPOl8rzMTlvS0hWdmTUEwnYpKBTwutNnEHZPfpqs8l0ndE9q0xy3bMAiCnufloR8/xnNJLHpnmljTCXdu8rk7KlGnElI9kGguzS97faAJ3UmU3OCd8vLTF9bfoBvTGJsFqBL5fV//V/fe6hRq6Q7QtWw/xKZz6z/zlaQg/RsOqwTKzermTdCSDQH91KE0rK9FvKIs4dYoU8CEuzVzQMSkhM1o= 99 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Variables to setup through environment 3 | #------------------------------------------------------------------------------- 4 | 5 | # Specify default values. 6 | prefix := /usr/local 7 | destdir := 8 | system := linux 9 | # Fallback to defaults but allow to get the values from environment. 10 | PREFIX ?= $(prefix) 11 | BINDIR ?= $(PREFIX)/bin 12 | DESTDIR ?= $(destdir) 13 | SYSTEM ?= $(system) 14 | 15 | #------------------------------------------------------------------------------- 16 | # Installation paths and defaults 17 | #------------------------------------------------------------------------------- 18 | 19 | SHELL := /bin/bash 20 | DIRNAME := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 21 | RST2MAN := rst2man 22 | PROG := flow 23 | PROGSINGLE := flow.sh 24 | DATAPATHVAR := DATAPATH 25 | USAGEVAR := USAGE 26 | VERSIONVAR := VERF 27 | README := README.md 28 | MANFILE := $(PROG).1 29 | MANRST := $(PROG).1.rst 30 | USAGEFILE := $(PROG).usage 31 | VERFILE := VERSION 32 | CHLOGFILE := CHANGELOG.md 33 | DESTPATH := $(DESTDIR)$(PREFIX) 34 | BINPATH := $(DESTDIR)$(BINDIR) 35 | SHAREPATH := $(DESTPATH)/share 36 | MANDIR := man/man1 37 | DISTNAME := $(PROG)-$$(cat $(VERFILE))-$(SYSTEM) 38 | # COMPILEDIR is overriden by dist and distsingle recipes 39 | COMPILEDIR := compiled 40 | INSTFILE := install 41 | INSDEVTFILE := install_develop 42 | UNINSTFILE := uninstall 43 | USAGEHEADER := "Usage: " 44 | 45 | #------------------------------------------------------------------------------- 46 | # Canned recipes 47 | #------------------------------------------------------------------------------- 48 | 49 | # Extract text from README between headers and format it to troff syntax 50 | define compile_usage 51 | @ echo -n "Compiling usage file ..." 52 | @ echo -n "$(USAGEHEADER)" > $(COMPILEDIR)/$(USAGEFILE) 53 | @ grep "^$(PROG) \[" $(MANRST) | sed 's/\\|/|/g' >> $(COMPILEDIR)/$(USAGEFILE) 54 | @ echo ".TH" >> $(COMPILEDIR)/$(USAGEFILE) 55 | @ sed -n '/^OPTIONS/,/^EXIT CODES/p' $(MANRST) | grep -v "^\(EXIT CODES\|OPTIONS\|======\)" \ 56 | | sed 's/^\\//;s/^-/.TP 18\n-/' | sed 's/^ //' | sed '/^$$/d' >> $(COMPILEDIR)/$(USAGEFILE) 57 | @ echo DONE 58 | endef 59 | 60 | #------------------------------------------------------------------------------- 61 | # Recipes 62 | #------------------------------------------------------------------------------- 63 | 64 | compile: 65 | @ mkdir -p $(COMPILEDIR) 66 | @ cp $(VERFILE) $(CHLOGFILE) $(COMPILEDIR) 67 | 68 | @ # Insert default datapath variable into $(PROG) 69 | @ echo -n "Compiling command file ..." 70 | @ { \ 71 | head -n1 $(PROG); \ 72 | echo "$(DATAPATHVAR)=\"$(SHAREPATH)/$(PROG)\""; \ 73 | tail -n+2 $(PROG); \ 74 | } > $(COMPILEDIR)/$(PROG) 75 | @ chmod +x $(COMPILEDIR)/$(PROG) 76 | @ echo DONE 77 | 78 | @ # Extract text from README between headers and convert it to troff syntax 79 | @ echo -n "Compiling man file ..." 80 | @ { \ 81 | echo -n ".TH \"FLOW\" \"1\" "; \ 82 | echo -n "\""; echo -n $$(stat -c %z $(MANRST) | cut -d" " -f1); echo -n "\" "; \ 83 | echo -n "\"User Manual\" "; \ 84 | echo -n "\"Version "; echo -n $$(cat $(VERFILE)); echo -n "\" "; \ 85 | echo; \ 86 | } > $(COMPILEDIR)/$(MANFILE) 87 | @ sed 's/`\([^`]\+\)<\([^>]\+\)>`__/\1\n \2/g' $(MANRST) | $(RST2MAN) | tail -n+33 >> $(COMPILEDIR)/$(MANFILE) 88 | @ echo DONE 89 | 90 | @ # Copy README and MAN rst into COMPILEDIR 91 | @ echo -n "Compiling readme file ..." 92 | @ cp $(README) $(COMPILEDIR)/$(README) 93 | @ cp $(MANRST) $(COMPILEDIR)/$(MANRST) 94 | @ echo DONE 95 | 96 | $(compile_usage) 97 | 98 | @ echo -n "Compiling install file ..." 99 | @ { \ 100 | echo "#!/bin/bash"; \ 101 | echo; \ 102 | echo ": \$${BINPATH:=$(BINPATH)}"; \ 103 | echo ": \$${SHAREPATH:=$(SHAREPATH)}"; \ 104 | echo ": \$${USRMANPATH:=\$$SHAREPATH/$(MANDIR)}"; \ 105 | echo; \ 106 | echo "dir=\"\$$(dirname \"\$$0\")\""; \ 107 | echo "mkdir -p \"\$$USRMANPATH\" \\"; \ 108 | echo "&& cp \"\$$dir/$(MANFILE)\" \"\$$USRMANPATH\" \\"; \ 109 | echo "&& mkdir -p \"\$$BINPATH\" \\"; \ 110 | echo "&& cp \"\$$dir/$(PROG)\" \"\$$BINPATH\" \\"; \ 111 | echo "&& mkdir -p \"\$$SHAREPATH/$(PROG)\" \\"; \ 112 | echo "&& cp \"\$$dir/$(USAGEFILE)\" \"\$$dir/$(VERFILE)\" \"\$$SHAREPATH/$(PROG)\" \\"; \ 113 | echo "&& echo 'Installation completed.' \\"; \ 114 | echo "|| { echo 'Installation failed.'; exit 1; }"; \ 115 | } > $(COMPILEDIR)/$(INSTFILE) 116 | @ chmod +x $(COMPILEDIR)/$(INSTFILE) 117 | @ echo DONE 118 | 119 | @ echo -n "Compiling uninstall file ..." 120 | @ { \ 121 | echo "#!/bin/bash"; \ 122 | echo; \ 123 | echo ": \$${BINPATH:=$(BINPATH)}"; \ 124 | echo ": \$${SHAREPATH:=$(SHAREPATH)}"; \ 125 | echo ": \$${USRMANPATH:=\$$SHAREPATH/$(MANDIR)}"; \ 126 | echo; \ 127 | echo "rm \"\$$USRMANPATH/$(MANFILE)\""; \ 128 | echo "rm \"\$$BINPATH/$(PROG)\""; \ 129 | echo "rm -rf \"\$$SHAREPATH/$(PROG)\""; \ 130 | echo "echo 'Uninstallation completed.'"; \ 131 | } > $(COMPILEDIR)/$(UNINSTFILE) 132 | @ chmod +x $(COMPILEDIR)/$(UNINSTFILE) 133 | @ echo DONE 134 | 135 | dist: COMPILEDIR=$(DISTNAME) 136 | dist: compile 137 | @ tar czf $(COMPILEDIR).tar.gz $(COMPILEDIR) 138 | @ echo "Distribution built; see 'tar tzf $(COMPILEDIR).tar.gz'" 139 | 140 | distsingle: COMPILEDIR=. 141 | distsingle: 142 | @ $(compile_usage) 143 | 144 | @ echo -n "Compiling single script ..." 145 | @ # Insert content of $(USAGEFILE) and $(VERFILE) into $(PROG) as variables 146 | @ { \ 147 | head -n1 $(PROG); \ 148 | echo "$(USAGEVAR)=\"$$(cat $(USAGEFILE))\""; \ 149 | echo "$(VERSIONVAR)=\"$$(cat $(VERFILE))\""; \ 150 | tail -n+2 $(PROG); \ 151 | } > $(PROGSINGLE) 152 | @ chmod +x $(PROGSINGLE) 153 | @ echo DONE 154 | 155 | clean: 156 | @ rm -rf $(COMPILEDIR) 157 | @ rm -rf $(DISTNAME) 158 | @ rm -f $(USAGEFILE) 159 | 160 | distclean: 161 | @ rm -f *.tar.gz 162 | @ rm -f $(PROGSINGLE) 163 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.1.2] - 2025-11-11 10 | 11 | ### Fixed 12 | 13 | - Setting and restoring the terminal flags with stty from uutils coreutils (#88). 14 | 15 | ## [3.1.1] - 2025-04-18 16 | 17 | ### Fixed 18 | 19 | - Ignore origin/origin remote branch. 20 | 21 | ## [3.1.0] - 2024-09-19 22 | 23 | _Stable release based on [3.1.0-rc.1]._ 24 | 25 | ## [3.1.0-rc.1] - 2024-09-19 26 | 27 | ### Changed 28 | 29 | - Do not automatically merge conflicts. 30 | - Consolidate pull and push and their outputs. 31 | 32 | ## [3.0.11] - 2023-10-06 33 | 34 | ### Fixed 35 | 36 | - Version validation format single-digit patch number. 37 | 38 | ## [3.0.10] - 2023-07-17 39 | 40 | ### Fixed 41 | 42 | - Fix setting github env. 43 | 44 | ## [3.0.9] - 2023-07-17 45 | 46 | ### Fixed 47 | 48 | - Fix actions syntax 49 | 50 | ## [3.0.8] - 2023-07-17 51 | 52 | ### Fixed 53 | 54 | - Generate asset names automatically. 55 | 56 | ## [3.0.7] - 2023-07-17 57 | 58 | ### Fixed 59 | 60 | - Add assets content type. 61 | 62 | ## [3.0.6] - 2023-07-17 63 | 64 | ### Fixed 65 | 66 | - Add release assets name. 67 | 68 | ## [3.0.5] - 2023-07-17 69 | 70 | ### Fixed 71 | 72 | - Fix upload release asset id. 73 | 74 | ## [3.0.4] - 2023-07-17 75 | 76 | ### Fixed 77 | 78 | - Upload release assets automatically. 79 | 80 | ## [3.0.3] - 2023-07-17 81 | 82 | ### Fixed 83 | 84 | - Fix automatic release body. 85 | 86 | ## [3.0.2] - 2023-07-17 87 | 88 | ### Fixed 89 | 90 | - Fix automatic releases chnagelog. 91 | 92 | ## [3.0.1] - 2023-07-17 93 | 94 | ### Fixed 95 | 96 | - Automatic release not working 97 | 98 | ## [3.0.0] - 2023-07-17 99 | 100 | _Stable release based on [3.0.0-rc.2]._ 101 | 102 | ## [3.0.0-rc.2] - 2023-07-17 103 | 104 | ### Added 105 | - All key branches validation on existence and merge status. 106 | - Version validation across all key branches. 107 | - Tag validation on the main branch. 108 | - Separate stable branch for each major version, e.g. `prod-1`. 109 | - Key branch names adaptation to pre-existing branches. 110 | - Option --auto-entry to skip changelog prompt. 111 | - Consider remote branches if key branch not found. 112 | - Local branch behind remote validation. 113 | 114 | ### Changed 115 | - Exit codes validation error fixable and unfixable. 116 | - Default branch names changed to main and staging. 117 | - Options --conform and --init require confirmations (forceable with --yes). 118 | - Merge staging branch into main after releasing (do not delete). 119 | - Auto version increment after releasing dev branch. 120 | - Keyword argument redesign. 121 | - Option --what-now rephrased. 122 | - Option --dry-run also checks attributes after validation. 123 | - Documentation updated with new git flow example. 124 | - Insert default changelog entry if empty or skipped. 125 | - Environmental variable names now start with 'GF_'. 126 | - Merge hotfix according to its major version number. 127 | - Show usage after all invalid option error. 128 | - Show hint after all fixable validation errors. 129 | - Project renamed from 'omgf' to 'flow' 130 | 131 | ## [3.0.0-rc.1] - 2023-06-13 132 | 133 | ## [2.2.0] - 2017-06-05 134 | ### Changed 135 | - Increment minor version when release is created #31 136 | 137 | ### Fixed 138 | - Initialization checks only local branches #73 139 | - `omgf pull` updates only local branches with remote branch #59 140 | 141 | ## [2.1.2] - 2017-04-10 142 | ### Fixed 143 | - Repair --help option #64 144 | 145 | ## [2.1.1] - 2017-04-07 146 | ### Fixed 147 | - Show links URL in man page #57 148 | 149 | ## [2.1.0] - 2017-04-03 150 | ### Added 151 | - Support for macOS #41 152 | - Check for requirements upon start, document requirements in README #30 153 | 154 | ### Changed 155 | - Improve README: add Setup and Alternatives, add OMGF's output and simplify installation instructions. 156 | - Use `env` instead of hardcoded path to Bash in shebang 157 | 158 | ### Fixed 159 | - Fix Unreleased URL generated for CHANGELOG 160 | 161 | ## [2.0.2] - 2017-03-11 162 | ### Fixed 163 | - Fix load user options to be case-sensitive #27 164 | - `make clean` and `make distclean` force removes files #28 165 | 166 | ## [2.0.1] - 2017-03-06 167 | ### Fixed 168 | - Fix `make dist` 169 | - Fix README.md Install section 170 | 171 | ## [2.0.0] - 2017-03-05 172 | 173 | ### Added 174 | - Add [EditorConfig](http://editorconfig.org/) file to enforce standard formatting 175 | 176 | ### Changed 177 | - Rename gf to omgf 178 | - Makefile uses BINDIR instead of EXEC_PREFIX 179 | 180 | ## [1.1.1] - 2017-01-30 181 | 182 | ## [1.1.0] - 2017-01-30 183 | ### Added 184 | - Automatic deployment into GitHub releases 185 | - `make distsingle` target compiles gf into a single file 186 | 187 | ## [1.0.1] - 2017-01-02 188 | ### Fixed 189 | - Proper changelog keywords listing 190 | 191 | ## [1.0.0] - 2016-12-22 192 | 193 | [Unreleased]: https://https://github.com/internetguru/flow/compare/staging...dev 194 | [3.1.2]: https://https://github.com/InternetGuru/flow/compare/v3.1.1...v3.1.2 195 | [3.1.1]: https://https://github.com/internetguru/flow/compare/v3.1.0...v3.1.1 196 | [3.1.0]: https://https://github.com/internetguru/flow/compare/v3.0.11...v3.1.0 197 | [3.1.0-rc.1]: https://github.com/internetguru/flow/releases/tag/v3.0.11 198 | [3.0.11]: https://https://github.com/internetguru/flow/compare/v3.0.10...v3.0.11 199 | [3.0.10]: https://https://github.com/internetguru/flow/compare/v3.0.9...3.0.10 200 | [3.0.9]: https://https://github.com/internetguru/flow/compare/v3.0.8...v3.0.9 201 | [3.0.8]: https://https://github.com/internetguru/flow/compare/v3.0.7...v3.0.8 202 | [3.0.7]: https://https://github.com/internetguru/flow/compare/v3.0.6...v3.0.7 203 | [3.0.6]: https://https://github.com/internetguru/flow/compare/v3.0.5...v3.0.6 204 | [3.0.5]: https://https://github.com/internetguru/flow/compare/v3.0.4...v3.0.5 205 | [3.0.4]: https://https://github.com/internetguru/flow/compare/v3.0.3...v3.0.4 206 | [3.0.3]: https://https://github.com/internetguru/flow/compare/v3.0.2...v3.0.3 207 | [3.0.2]: https://https://github.com/internetguru/flow/compare/v3.0.1...v3.0.2 208 | [3.0.1]: https://https://github.com/internetguru/flow/compare/v3.0.0...v3.0.1 209 | [3.0.0]: https://https://github.com/internetguru/flow/compare/v2.2.0...v3.0.0 210 | [3.0.0-rc.2]: https://github.com/internetguru/flow/releases/tag/v2.2.0 211 | [3.0.0-rc.1]: https://github.com/internetguru/flow/releases/tag/v2.2.0 212 | [2.2.0]: https://github.com/internetguru/flow/compare/v2.1.2...v2.2.0 213 | [2.1.2]: https://github.com/internetguru/flow/compare/v2.1.1...v2.1.2 214 | [2.1.1]: https://github.com/internetguru/flow/compare/v2.1.0...v2.1.1 215 | [2.1.0]: https://github.com/internetguru/flow/compare/v2.0.2...v2.1.0 216 | [2.0.2]: https://github.com/internetguru/flow/compare/v2.0.1...v2.0.2 217 | [2.0.1]: https://github.com/internetguru/flow/compare/v2.0.0...v2.0.1 218 | [2.0.0]: https://github.com/internetguru/flow/compare/v1.1.1...v2.0.0 219 | [1.1.1]: https://github.com/internetguru/flow/compare/v1.1.0...v1.1.1 220 | [1.1.0]: https://github.com/internetguru/flow/compare/v1.0.1...v1.1.0 221 | [1.0.1]: https://github.com/internetguru/flow/compare/v1.0.0...v1.0.1 222 | [1.0.0]: https://github.com/internetguru/flow/compare/v0.0.0...v1.0.0 223 | -------------------------------------------------------------------------------- /flow.1.rst: -------------------------------------------------------------------------------- 1 | NAME 2 | ==== 3 | 4 | Flow - manage git branching model 5 | 6 | 7 | SYNOPSIS 8 | ======== 9 | 10 | flow [-cefhinrvVwy] [--color[=WHEN]] [--push] [--pull] [BRANCH] 11 | 12 | 13 | DESCRIPTION 14 | =========== 15 | 16 | Advance in a branching model according to the current branch or a branch specified with an argument. For most existing branches, the default action is release. If a given branch does not exist, Flow creates it as a feature or a hotfix depending on the current branch or a keyword (feature/hotfix). 17 | 18 | Additionally, Flow handles version incrementing and maintains a changelog. Before proceeding, it verifies the current repository for branching model compliance and offers to correct any detected imperfections. 19 | 20 | `Read more about Flow `__ 21 | 22 | 23 | OPTIONS 24 | ======= 25 | 26 | \-c, --conform 27 | Repair (initialize) project to be conform with git flow branching model and proceed. 28 | 29 | \--color[=WHEN], --colour[=WHEN] 30 | Use markers to highlight command status; WHEN is 'always', 'never', or 'auto'. Empty WHEN sets color to 'always'. Default color value is 'auto'. 31 | 32 | \-e, --auto-entry 33 | Do not show changelog editor and insert general entry instead about the current action. 34 | 35 | \-f, --force 36 | Clear and restore uncommitted changes before proceeding using git stash. 37 | 38 | \-h, --help 39 | Print help. 40 | 41 | \-i, --init 42 | Same as 'conform', but also initialize git repository if not exists and do not proceed with any action. 43 | 44 | \-n, --dry-run 45 | Do not run commands; only parse user options and arguments. 46 | 47 | \--pull 48 | Pull all remote branches. 49 | 50 | \--push 51 | Push all branches. 52 | 53 | \-r, --request 54 | Instead of merging prepare current branch for pull request and push it to the origin. 55 | 56 | \-v, --verbose 57 | Verbose mode. 58 | 59 | \-V, --version 60 | Print version number. 61 | 62 | \-w, --what-now 63 | Display what to do on current branch. 64 | 65 | \-y, --yes 66 | Assume yes for all questions. 67 | 68 | 69 | EXIT CODES 70 | ========== 71 | 72 | 0 73 | No problems occurred. 74 | 1 75 | Generic error code. 76 | 2 77 | Parse or invalid option error. 78 | 3 79 | Nonconforming, fixable with --conform / --init. 80 | 4 81 | Nonconforming, cannot be fixed automatically. 82 | 5 83 | Uncommitted changes. 84 | 6 85 | Nothing to do, e. g. empty merge. 86 | 7 87 | Unexpected merge conflicts. 88 | 89 | REPOSITORY 90 | ========== 91 | 92 | `Flow on GitHub repository `__ 93 | 94 | 95 | REPORTING BUGS 96 | ============== 97 | 98 | `Issue tracker `__ 99 | 100 | 101 | AUTHOR 102 | ====== 103 | 104 | Written by Pavel Petrzela and George J. Pavelka. 105 | 106 | 107 | COPYRIGHT 108 | ========= 109 | 110 | Copyright © 2016--2023 `Internet Guru `__ 111 | 112 | This software is licensed under the CC BY-NC-SA license. There is NO WARRANTY, to the extent permitted by law. See the LICENSE file. 113 | 114 | For commercial use, a nominal fee may be applicable based on the company size and the nature of their product. In many instances, this could result in no fees being charged at all. Please contact us at info@internetguru.io for further information. 115 | 116 | Please do not hesitate to reach out to us for inquiries related to seminars, workshops, training, integration, support, custom development, and additional services. We are more than happy to assist you. 117 | 118 | 119 | DONATION 120 | ======== 121 | 122 | If you find this script useful, please consider making a donation to support its developers. We appreciate any contributions, no matter how small. Donations help us to dedicate more time and resources to this project, including covering our infrastructure expenses. 123 | 124 | `PayPal Donation `__ 125 | 126 | Please note that we are not a tax-exempt organization and cannot provide tax deductions for your donation. However, for donations exceeding $500, we would like to acknowledge your contribution on project's page and in this file (including the man page). 127 | 128 | Thank you for your continued support! 129 | 130 | 131 | HONORED DONORS 132 | ============== 133 | 134 | `Czech Technical University in Prague `__ 135 | 136 | `WebExpo Conference in Prague `__ 137 | 138 | `DATAMOLE data mining and machine learning `__ 139 | 140 | 141 | FLOW EXAMPLE 142 | ============ 143 | 144 | 1. Initialize the branching model on an empty folder:: 145 | 146 | mkdir myflow 147 | cd myflow 148 | flow --init --yes 149 | 150 | This creates a git repository with key branches and a tag. The default version number is ``0.0.0`` on all branches except for dev, where it is ``0.1.0``. The --yes option serves to skip prompting individual steps. 151 | 152 | 2. Create and release a feature:: 153 | 154 | flow --yes feature 155 | touch a 156 | git add a 157 | git commit -m "Add file a" 158 | flow --yes --auto-entry 159 | 160 | This creates a feature branch from dev and merges it back after changes are made. Without the --yes and the --auto-entry options, Flow prompts for a confirmation and a changelog entry respectively. 161 | 162 | 3. Fix some bugs on dev and release it:: 163 | 164 | touch b 165 | git add b 166 | git commit -m "Add file b" 167 | flow --yes 168 | 169 | This makes changes directly on development branch and releases it. No argument is necessary as releasing is the default action for most branches. 170 | 171 | Notice the version number ``0.1.0`` from dev branch moves to the staging branch and gets incremented on dev to ``0.2.0``. The stable branch (main) is still ``0.0.0``. You can use the following set of commands to check it:: 172 | 173 | git show dev:VERSION 174 | git show staging:VERSION 175 | git show main:VERSION 176 | 177 | 4. Fix some bugs on the staging branch and release:: 178 | 179 | touch c 180 | git add c 181 | git commit -m "Add file c" 182 | flow --yes --conform 183 | 184 | Ideally, every commit of the staging branch must be merged into dev. The script recognizes the unmerged state and fixes it using the --conform option while advancing with the release. 185 | 186 | Note: The staging branch and both production branches ('main' and 'main-0') are now on the same commit. There is also a tag with the newly released version number. This may seem a little far fetched. It will make more sense over time as the project grows. 187 | 188 | 5. Hotfix the production:: 189 | 190 | flow --yes hotfix 191 | touch d 192 | git add d 193 | git commit -m "Add file d" 194 | flow --yes --auto-entry 195 | 196 | This increments the patch version and merges the hotfix to the main branch, creates a tag and advances all attached branches with it. To keep the model compliant, it also merges the main branch into dev. 197 | 198 | Note: The git log graph may now look somewhat confusing. It will make much more sense during real development. If you want to see it, use the following command:: 199 | 200 | git log --oneline --decorate --color --graph --all 201 | 202 | Note: Check out the resulting changelog file if you want. It contains the added feature, hotfix, and all releases. The changelog on the development branch has additionally an 'unreleased' section:: 203 | 204 | git show main:CHANGELOG.md 205 | git diff main:CHANGELOG.md dev:CHANGELOG.md 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Internet Guru Flow 2 | 3 | | branch | status | 4 | | :------------- | :------------- | 5 | | master | ![tests](https://github.com/internetguru/flow/actions/workflows/test.yml/badge.svg?branch=master) | 6 | | dev | ![tests](https://github.com/internetguru/flow/actions/workflows/test.yml/badge.svg?branch=dev) | 7 | 8 | Advance in a branching model according to the current branch or a branch specified with an argument. For most existing branches, the default action is release. If a given branch does not exist, Flow creates it as a feature or a hotfix depending on the current branch or a keyword (feature/hotfix). 9 | 10 | Additionally, Flow handles version incrementing and maintains a changelog. Before proceeding, it verifies the current repository for branching model compliance and offers to correct any detected imperfections. 11 | 12 | - [Installation](#installation) 13 | - [Flow options](https://github.com/internetguru/flow/blob/dev/flow.1.rst#options) 14 | - [Usage example](https://github.com/internetguru/flow/blob/dev/flow.1.rst#flow-example) 15 | - [Blog posts on Flow](https://blog.internetguru.io/categories/flow/) 16 | 17 | ### Branching model automation 18 | 19 | - Flow requires *no arguments* and derives a default action. 20 | - Flow switches between branches accordingly and advises what to do next. 21 | - Flow can create pull requests instead of releasing directly. 22 | - Flow maintains separate production branches for major versions, such as `prod-1`. 23 | - Flow supports parallel hotfixing, even for separate production branches. 24 | 25 | ### Branching model validation 26 | 27 | - Flow validates and automatically *fixes project structures* to conform to the branching model. 28 | - Flow pulls and pushes all key branches and checks whether local branches are not behind. 29 | - Flow handles [semantic versioning](https://semver.org/) across all key branches. Read more about [version handling with Flow](https://blog.internetguru.io/2023/04/05/flow-version/). 30 | - Flow keeps track of a release history with the [Keep a CHANGELOG](https://keepachangelog.com/en/) convention. Read more about [changelog handling with Flow](https://blog.internetguru.io/2023/04/08/flow-changelog/). 31 | 32 | ### Setup and configuration 33 | 34 | - Flow can initiate a git branching repository in any folder with or without files. 35 | - Flow can convert any existing git repository to a git branching model. 36 | - Flow automatically adapts to existing branches, such as 'release' instead of the default 'staging'. 37 | 38 | ## Installation 39 | 40 | Download the [latest release from GitHub](https://github.com/internetguru/flow/releases/latest). You can install as a single file (easiest), with compiled distribution package (useful for system-wide install) or from the source. 41 | 42 | ### Requirements 43 | 44 | - [Bash](https://www.gnu.org/software/bash/), version 3.2 or later 45 | - [Git](https://git-scm.com/), version 1.8.0 or later 46 | - [GNU getopt](http://frodo.looijaard.name/project/getopt) 47 | - On macOS install with Homebrew ([`gnu-getopt`](http://braumeister.org/formula/gnu-getopt)) or with [MacPorts](https://www.macports.org/) (`getopt`) 48 | - [GNU sed](https://www.gnu.org/software/sed/) 49 | - On macOS install with Homebrew [`gnu-sed`](http://braumeister.org/formula/gnu-sed) 50 | - [GNU awk](https://www.gnu.org/software/gawk/) 51 | - On macOS install with Homebrew [`homebrew/dupes/grep`](https://github.com/Homebrew/homebrew-dupes) 52 | 53 | ### Single file script 54 | 55 | 1. Place flow.sh into your `$PATH` (e.g. `~/bin`). 56 | 2. Make the script executable. 57 | ```bash 58 | chmod +x flow.sh 59 | ``` 60 | 61 | ### Compiled distribution package 62 | 63 | 1. Extract the archive. 64 | ```bash 65 | tar -xvzf flow-*-linux.tar.gz 66 | ``` 67 | 2. run `install` script as root; this will proceed a system-wide installation into `/usr/local`. 68 | ```bash 69 | cd flow-*-linux 70 | sudo ./install 71 | ``` 72 | 73 | You can override installation paths using environment variables. 74 | 75 | - `BINPATH`: where the script will be placed, `/usr/local/bin` by default. 76 | - `SHAREPATH`: where support files will be placed, `/usr/local/share` by default. 77 | - `USRMANPATH`: where manpage will be placed, `$SHAREPATH/man/man1` by default. 78 | 79 | This is how to install the script without root permissions. 80 | 81 | ```bash 82 | BINPATH=~/bin SHAREPATH=~/.local/share ./install 83 | ``` 84 | 85 | ### Building from source 86 | 87 | You will need the following dependencies: 88 | 89 | - GNU Make 90 | - `rst2man` (available in Docutils, e.g. `apt-get install python-docutils` or `pip install docutils`) 91 | 92 | ```bash 93 | git clone https://github.com/internetguru/flow.git 94 | cd flow 95 | ./configure && make && sudo compiled/install 96 | ``` 97 | 98 | You can specify following variables for `make` command which will affect default parameters of `install` script: 99 | 100 | - `PREFIX`: Installation prefix, `/usr/local` by default. 101 | - `BINDIR`: Location for `flow` script, `$PREFIX/bin` by default. 102 | 103 | For example like this: 104 | 105 | ```bash 106 | PREFIX=/usr make 107 | ``` 108 | 109 | See the [man page](flow.1.rst) for more information and examples. 110 | 111 | ### Running unit tests 112 | 113 | Testing the script requires a built 'flow' command and [Bash Unit Testing Tool](https://github.com/internetguru/butt) -- AKA the 'butt' command. 114 | 115 | ```bash 116 | butt ~/flow/test/test.butt 117 | ``` 118 | 119 | ## Contributing 120 | 121 | Pull requests are welcome. Don't hesitate to contribute. 122 | 123 | ## Copyright 124 | 125 | Copyright © 2016--2023 [Internet Guru](https://www.internetguru.io) 126 | 127 | This software is licensed under the CC BY-NC-SA license. There is NO WARRANTY, to the extent permitted by law. See the [LICENSE](LICENSE) file. 128 | 129 | For commercial use, a nominal fee may be applicable based on the company size and the nature of their product. In many instances, this could result in no fees being charged at all. Please contact us at info@internetguru.io for further information. 130 | 131 | Please do not hesitate to reach out to us for inquiries related to seminars, workshops, training, integration, support, custom development, and additional services. We are more than happy to assist you. 132 | 133 | ## Donation 134 | 135 | If you find this script useful, please consider making a donation to support its developers. We appreciate any contributions, no matter how small. Donations help us to dedicate more time and resources to this project, including covering our infrastructure expenses. 136 | 137 | [PayPal Donation](https://www.paypal.com/donate/?hosted_button_id=QC7HU967R4PHC) 138 | 139 | Please note that we are not a tax-exempt organization and cannot provide tax deductions for your donation. However, for donations exceeding $500, we would like to acknowledge your contribution on project's page and in this file (including the man page). 140 | 141 | Thank you for your continued support! 142 | 143 | ### Honored donors 144 | 145 | - [Czech Technical University in Prague](https://www.fit.cvut.cz/en) 146 | - [WebExpo Conference in Prague](https://webexpo.net/) 147 | - [DATAMOLE data mining and machine learning](https://www.datamole.cz/) 148 | 149 | ## Alternatives 150 | 151 | - [git-flow](https://github.com/nvie/gitflow) – The original Vincent Driessen's tools. 152 | - [git-flow (AVH Edition)](https://github.com/petervanderdoes/gitflow-avh) – Maintained fork of the original tools. 153 | - See also [cheatsheet](https://danielkummer.github.io/git-flow-cheatsheet/) 154 | - [HubFlow](https://datasift.github.io/gitflow/) – Git Flow for GitHub by DataSift. 155 | - [gitflow4idea](https://github.com/OpherV/gitflow4idea/) – Plugin for JetBrains IDEs. 156 | - [GitKraken](https://www.gitkraken.com/) – Cross-platform Git GUI with [Git Flow operations](https://support.gitkraken.com/repositories/git-flow). 157 | - [SourceTree](https://www.sourcetreeapp.com/) – Git GUI for macOS and Windows with Git Flow support. 158 | - [GitFlow for Visual Studio](https://marketplace.visualstudio.com/items?itemName=vs-publisher-57624.GitFlowforVisualStudio2017) 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | -------------------------------------------------------------------------------- /test/test.butt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env butt 2 | 3 | function take { 4 | cd "${curdir}" \ 5 | && rm -rf "${1}" \ 6 | && mkdir "${1}" \ 7 | && cd "${1}" \ 8 | || exit 2 9 | } 10 | 11 | # make git return only error to stderr 12 | function gch { 13 | local out 14 | out="$(git checkout "$@" 2>&1)" \ 15 | || err "${out}" 16 | } 17 | 18 | function git_current_branch { 19 | git rev-parse --abbrev-ref HEAD 20 | } 21 | 22 | function gsp { 23 | git status --porcelain 24 | } 25 | 26 | function checkInitRefs { 27 | assert_equal "$(git show-ref | wc -l)" 5 28 | assert_contains "$(git show-ref)" "refs/heads/dev" 29 | assert_contains "$(git show-ref)" "refs/heads/${1:-main}" 30 | assert_contains "$(git show-ref)" "refs/heads/staging" 31 | assert_contains "$(git show-ref)" "refs/heads/${1:-main}-0" 32 | assert_contains "$(git show-ref)" "refs/tags/v0.0.0" 33 | } 34 | 35 | function checkInitFiles { 36 | assert_equal "$(git show dev:VERSION)" "0.1.0" 37 | assert_equal "$(git show "${1:-main}":VERSION)" "0.0.0" 38 | assert_equal "$(git show staging:VERSION)" "0.0.0" 39 | assert_equal "$(git show "${1:-main}"-0:VERSION)" "0.0.0" 40 | assert_equal "$(head -n1 CHANGELOG.md)" "# Change Log" 41 | } 42 | 43 | function grep_log { 44 | git log --no-color --oneline --decorate --all | grep --quiet "${1}" 45 | } 46 | 47 | function load_log { 48 | log="$(git log --no-color --oneline --decorate --all)" 49 | IFS=$'\n' read -d '' -r -a loglines <<< "${log}" || return 0 50 | } 51 | 52 | export FLOW_OPTIONS="" 53 | export COLUMNS=1000 54 | export MANWIDTH=1000 55 | 56 | #local i curdir ref log loglines chglog chgloglines 57 | i=0 58 | curdir="$(pwd)" 59 | ref= 60 | log= 61 | loglines=() 62 | outlines=() 63 | errlines=() 64 | status= 65 | 66 | ####################################### 67 | ## Integration 68 | ####################################### 69 | # TODO test flow help on macOs, now it is skipped due to issue https://github.com/InternetGuru/flow/issues/66 70 | : $((i++)) 71 | start "flow help" 72 | take "${i}" 73 | debug "flow --help" 74 | assert_equal "${status}" 0 75 | assert_startwith "${outlines[0]}" "Usage: flow [-" 76 | assert_startwith "${outlines[1]}" " -c, --conform" 77 | debug "flow -n" 78 | assert_equal "${status}" 3 79 | end 80 | ####################################### 81 | : $((i++)) 82 | start "flow --version shows version" 83 | take "${i}" 84 | debug "flow --version" 85 | assert_equal "${status}" 0 86 | assert_startwith "${outlines[0]}" "flow " 87 | debug "flow -n" 88 | assert_equal "${status}" 3 89 | end 90 | ####################################### 91 | : $((i++)) 92 | start "flow -V shows version" 93 | take "${i}" 94 | debug "flow -V" 95 | assert_equal "${status}" 0 96 | assert_startwith "${outlines[0]}" "flow " 97 | debug "flow -n" 98 | assert_equal "${status}" 3 99 | end 100 | ####################################### 101 | # : $((i++)) 102 | # start "man flow" 103 | # take "${i}" 104 | # debug "man 2>/dev/null flow" 105 | # assert_equal "${status}" 0 106 | # assert_startwith "${outlines[0]}" "GF(1)" 107 | # debug "flow -n" 108 | # assert_equal "${status}" 0 109 | # end 110 | ####################################### 111 | ## Error codes 112 | ####################################### 113 | : $((i++)) 114 | start "invalid option error code" 115 | take "${i}" 116 | debug "flow --invalid-option" 117 | assert_equal "${status}" 2 118 | assert_contains "${errlines[0]}" "unrecognized option" 119 | debug "flow -n" 120 | assert_equal "${status}" 3 121 | end 122 | ####################################### 123 | : $((i++)) 124 | start "missing git error code" 125 | take "${i}" 126 | debug "flow" 127 | assert_equal "${status}" 3 128 | # shellcheck disable=SC2154 129 | assert_contains "${errlines[0]}" "Git repository not found." 130 | end 131 | ####################################### 132 | : $((i++)) 133 | start "missing branch dev error code" 134 | take "${i}" 135 | { flow -iy \ 136 | && gch main \ 137 | && git branch -D dev 138 | } >/dev/null 2>&1 139 | debug "flow" 140 | assert_equal "${status}" 3 141 | assert_contains "${errlines[0]}" "Local branch 'dev' not found." 142 | end 143 | ####################################### 144 | : $((i++)) 145 | start "missing VERSION file error code" 146 | take "${i}" 147 | { flow -iy \ 148 | && rm VERSION \ 149 | && git commit -am "delete VERSION file" 150 | } >/dev/null 151 | debug "flow" 152 | assert_equal "${status}" 3 153 | assert_contains "${errlines[0]}" "Missing or invalid version file on dev." 154 | end 155 | ####################################### 156 | : $((i++)) 157 | start "dev is behind main error code" 158 | take "${i}" 159 | { flow -iy \ 160 | && echo a > a \ 161 | && git add a \ 162 | && git commit -am "add file a" \ 163 | && flow --yes \ 164 | && flow --yes \ 165 | && gch dev \ 166 | && git reset --hard HEAD~1 167 | } >/dev/null 168 | debug "flow -v" 169 | assert_equal "${status}" 3 170 | assert_contains "${errlines[0]}" "Branch 'main' is not merged into 'dev'." 171 | end 172 | ####################################### 173 | : $((i++)) 174 | start "invalid changelog on dev" 175 | take "${i}" 176 | { flow -iy \ 177 | && echo a > CHANGELOG.md \ 178 | && git commit -am "break changelog" 179 | } >/dev/null 180 | debug "flow" 181 | assert_equal "${status}" 3 182 | assert_contains "${errlines[0]}" "Missing or invalid changelog file on dev." 183 | end 184 | ####################################### 185 | : $((i++)) 186 | start "uncommitted changes error code" 187 | take "${i}" 188 | { flow -iy \ 189 | && echo a > a 190 | } >/dev/null 191 | debug "flow" 192 | assert_equal "${status}" 5 193 | assert_contains "${errlines[0]}" "Uncommitted changes." 194 | end 195 | ####################################### 196 | : $((i++)) 197 | start "version change on dev" 198 | take "${i}" 199 | { flow -iy \ 200 | && echo 1.0.0 > VERSION \ 201 | && git commit -am "bump version" \ 202 | && flow --yes hotfix \ 203 | && echo a > a \ 204 | && git add . \ 205 | && git commit -am "fix a" 206 | } >/dev/null 207 | debug "echo '' | flow --yes" 208 | assert_equal "${status}" 0 209 | load_log 210 | assert_contains "${log}" "(HEAD -> dev) Merge branch 'main' into dev" 211 | assert_contains "${log}" "(tag: v0.0.1, staging, main-0, main) Merge branch 'hotfix-$(whoami)'" 212 | assert_contains "${log}" "Merge branch 'main' into dev" 213 | assert_equal "$(git show main:VERSION)" "0.0.1" 214 | assert_equal "$(git show staging:VERSION)" "0.0.1" 215 | assert_equal "$(git show dev:VERSION)" "1.0.0" 216 | assert_equal "$(git show-ref | grep /refs/heads/hotfix-$(whoami))" "" 217 | assert_equal "$(gsp)" "" 218 | debug "flow -n" 219 | assert_equal "${status}" 0 220 | end 221 | ####################################### 222 | ## Dry run 223 | ####################################### 224 | : $((i++)) 225 | start "dry run" 226 | take "${i}" 227 | debug "flow --dry-run" 228 | assert_equal "${status}" 3 229 | assert_contains "${errlines[0]}" "Git repository not found." 230 | end 231 | ####################################### 232 | : $((i++)) 233 | start "dry run and invalid option" 234 | take "${i}" 235 | debug "flow --dry-run --invalid-option" 236 | assert_equal "${status}" 2 237 | assert_contains "${errlines[0]}" "unrecognized option" 238 | end 239 | ####################################### 240 | ## Init 241 | ####################################### 242 | : $((i++)) 243 | start "init on empty folder" 244 | take "${i}" 245 | debug "flow --init --yes" 246 | assert_equal "${status}" 0 247 | checkInitFiles 248 | checkInitRefs 249 | assert_equal "$(gsp)" "" 250 | debug "flow -n" 251 | assert_equal "${status}" 0 252 | end 253 | ####################################### 254 | : $((i++)) 255 | start "init on non-empty folder" 256 | take "${i}" 257 | { touch a \ 258 | && touch b 259 | } >/dev/null 260 | debug "flow --init --yes" 261 | assert_equal "${status}" 5 262 | assert_contains "${errlines[0]}" "Folder is not empty." 263 | end 264 | ####################################### 265 | : $((i++)) 266 | start "force init on non-empty folder" 267 | take "${i}" 268 | { touch a; } >/dev/null 269 | debug "flow --init --yes --force" 270 | assert_equal "${status}" 0 271 | checkInitFiles 272 | assert_equal "$(git show-ref | wc -l)" 5 273 | assert_equal "$(gsp | wc -l)" 1 274 | debug "flow -n" 275 | assert_equal "${status}" 5 276 | end 277 | ####################################### 278 | : $((i++)) 279 | start "init on existing repo with commits" 280 | take "${i}" 281 | { git init -b master \ 282 | && echo a > a \ 283 | && git add . \ 284 | && git commit -am "first commit.." 285 | } >/dev/null 286 | debug "flow -v --init --yes" 287 | assert_equal "${status}" 0 288 | checkInitFiles master 289 | checkInitRefs master 290 | assert_equal "$(gsp)" "" 291 | debug "flow -n" 292 | assert_equal "${status}" 0 293 | end 294 | ####################################### 295 | : $((i++)) 296 | start "init on existing repo uncommitted" 297 | take "${i}" 298 | { git init \ 299 | && echo a > a 300 | } >/dev/null 301 | debug "flow -v --init --yes" 302 | assert_equal "${status}" 5 303 | assert_contains "${errlines[0]}" "Folder is not empty." 304 | end 305 | ####################################### 306 | : $((i++)) 307 | start "force init on existing repo without commits" 308 | take "${i}" 309 | { git init -b master \ 310 | && echo a > a 311 | } >/dev/null 312 | debug "flow -v --init --yes --force" 313 | assert_equal "${status}" 0 314 | checkInitFiles master 315 | assert_equal "$(git show-ref | wc -l)" 5 316 | assert_equal "$(gsp | wc -l)" 1 317 | debug "flow -n" 318 | assert_equal "${status}" 5 319 | end 320 | ####################################### 321 | ## Conform 322 | ####################################### 323 | : $((i++)) 324 | start "conform on empty folder" 325 | take "${i}" 326 | debug "yes no | flow --conform --yes" 327 | assert_equal "${status}" 3 328 | assert_contains "${errlines[0]}" "Git repository not found." 329 | end 330 | ####################################### 331 | : $((i++)) 332 | start "conform on existing repo without commits" 333 | take "${i}" 334 | { git init \ 335 | && echo a > a 336 | } >/dev/null 337 | debug "flow --conform --yes" 338 | assert_equal "${status}" 3 339 | assert_contains "${errlines[0]}" "Missing initial commit." 340 | end 341 | ####################################### 342 | : $((i++)) 343 | start "force init on existing repo with commits" 344 | take "${i}" 345 | { git init -b master \ 346 | && echo a > a \ 347 | && git add . \ 348 | && git commit -am "first commit" \ 349 | && echo b > b 350 | } >/dev/null 351 | debug "flow --init --yes --force" 352 | assert_equal "${status}" 0 353 | assert_equal "$(git_current_branch)" "dev" 354 | checkInitFiles master 355 | assert_equal "$(gsp | wc -l)" 1 356 | debug "flow -n" 357 | assert_equal "${status}" 5 358 | end 359 | ####################################### 360 | : $((i++)) 361 | start "force conform on existing repo with commits" 362 | take "${i}" 363 | { git init -b main \ 364 | && echo a > a \ 365 | && git add . \ 366 | && git commit -am "first commit" \ 367 | && echo b > b 368 | } >/dev/null 369 | debug "flow --conform --force --yes" 370 | assert_equal "${status}" 0 371 | assert_equal "$(git show-ref | wc -l)" 6 372 | assert_equal "$(git_current_branch)" "hotfix-$(whoami)" 373 | assert_equal "$(gsp | wc -l)" 1 374 | debug "flow -n" 375 | assert_equal "${status}" 5 376 | end 377 | ####################################### 378 | ## Feature 379 | ####################################### 380 | : $((i++)) 381 | start "create feature" 382 | take "${i}" 383 | { flow --init --yes; } >/dev/null 384 | debug "flow -v --yes feature" 385 | assert_equal "${status}" 0 386 | assert_contains "$(git show-ref)" "refs/heads/feature-$(whoami)" 387 | assert_equal "$(git_current_branch)" "feature-$(whoami)" 388 | debug "flow -n" 389 | assert_equal "${status}" 6 390 | end 391 | ####################################### 392 | : $((i++)) 393 | start "force create feature with uncommited changes" 394 | take "${i}" 395 | { flow --init --yes \ 396 | && echo a > a 397 | } >/dev/null 398 | debug "flow -v --yes --force myfeature" 399 | assert_equal "${status}" 0 400 | assert_equal "$(git show-ref | wc -l)" 6 401 | assert_contains "${outlines[0]}" "Stashing files" 402 | assert_equal "$(git_current_branch)" "myfeature" 403 | assert_equal "$(gsp | wc -l)" 1 404 | debug "flow -n" 405 | assert_equal "${status}" 5 406 | end 407 | ####################################### 408 | : $((i++)) 409 | start "merge feature" 410 | take "${i}" 411 | { flow --init --yes \ 412 | && flow --yes myfeature \ 413 | && echo a > a \ 414 | && git add . \ 415 | && git commit -am "add feature 1" 416 | } >/dev/null 417 | debug "echo 'feature1' | flow --yes" 418 | assert_equal "${status}" 0 419 | load_log 420 | assert_contains "${log}" "Update changelog" 421 | assert_contains "${loglines[0]}" "(HEAD -> dev) Merge branch 'myfeature' into dev" 422 | assert_equal "$(git show-ref | grep /refs/heads/feature-myfeature)" "" 423 | assert_equal "$(git show dev:CHANGELOG.md | head -9 | tail -1)" "### Added" 424 | assert_equal "$(git show dev:CHANGELOG.md | head -11 | tail -1)" "- feature1" 425 | assert_equal "$(gsp)" "" 426 | debug "flow -n" 427 | assert_equal "${status}" 0 428 | end 429 | ####################################### 430 | : $((i++)) 431 | start "merge empty feature" 432 | take "${i}" 433 | { flow --init --yes \ 434 | && flow --yes myfeature 435 | } >/dev/null 436 | debug "flow -v --yes" 437 | assert_equal "${status}" 6 438 | assert_contains "${errlines[0]}" "Nothing to merge." 439 | end 440 | ####################################### 441 | : $((i++)) 442 | start "force merge feature with uncommitted changes" 443 | take "${i}" 444 | { flow --init --yes \ 445 | && flow --yes myfeature \ 446 | && echo a > a \ 447 | && git add . \ 448 | && git commit -am "add feature 1" \ 449 | && echo b > b 450 | } >/dev/null 451 | debug "echo 'feature1' | flow --yes --force" 452 | assert_equal "${status}" 0 453 | #assert_contains "${outlines[0]}" "Stashing files" 454 | load_log 455 | assert_contains "${log}" "Update changelog" 456 | assert_contains "${loglines[0]}" "(HEAD -> dev) Merge branch 'myfeature' into dev" 457 | assert_equal "$(git show-ref | grep /refs/heads/myfeature)" "" 458 | assert_equal "$(gsp | wc -l)" 1 459 | debug "flow -n" 460 | assert_equal "${status}" 5 461 | end 462 | ####################################### 463 | # start "merge feature with --request" 464 | # { flow --init --yes \ 465 | # && flow --yes myfeature \ 466 | # && echo a > a \ 467 | # && git add . \ 468 | # && git commit -am "add feature 1" 469 | # } >/dev/null 470 | # debug "echo 'feature1' | flow --yes --request" 471 | # fi 472 | ####################################### 473 | ## RELEASE 474 | ####################################### 475 | : $((i++)) 476 | start "create staging" 477 | take "${i}" 478 | { flow --init --yes \ 479 | && echo a > a \ 480 | && git add . \ 481 | && git commit -m "add file a" 482 | } >/dev/null 483 | debug "flow -v --yes" 484 | assert_equal "${status}" 0 485 | assert_equal "$(git show dev:VERSION)" "0.2.0" 486 | assert_equal "$(git show staging:VERSION)" "0.1.0" 487 | assert_equal "$(git show dev:CHANGELOG.md | head -7 | tail -1)" "## [Unreleased]" 488 | assert_contains "$(git show dev:CHANGELOG.md | head -9 | tail -1)" "## [0.1.0-rc.1] - " 489 | assert_contains "$(git show staging:CHANGELOG.md | head -7 | tail -1)" "## [0.1.0-rc.1] - " 490 | assert_contains "$(git show-ref)" "refs/heads/staging" 491 | assert_equal "$(gsp)" "" 492 | debug "flow -n" 493 | assert_equal "${status}" 0 494 | end 495 | ####################################### 496 | : $((i++)) 497 | start "create rc2" 498 | take "${i}" 499 | { flow --init --yes \ 500 | && flow -y dev 501 | } >/dev/null 502 | debug "flow -y dev" 503 | assert_equal "${status}" 0 504 | assert_equal "$(git show dev:VERSION)" "0.2.0" 505 | assert_equal "$(git show staging:VERSION)" "0.1.0" 506 | assert_equal "$(git show dev:CHANGELOG.md | head -7 | tail -1)" "## [Unreleased]" 507 | assert_contains "$(git show dev:CHANGELOG.md | head -9 | tail -1)" "## [0.1.0-rc.2] - " 508 | assert_contains "$(git show dev:CHANGELOG.md | head -11 | tail -1)" "## [0.1.0-rc.1] - " 509 | assert_contains "$(git show staging:CHANGELOG.md | head -7 | tail -1)" "## [0.1.0-rc.2] - " 510 | assert_contains "$(git show staging:CHANGELOG.md | head -9 | tail -1)" "## [0.1.0-rc.1] - " 511 | assert_contains "$(git show-ref)" "refs/heads/staging" 512 | assert_equal "$(gsp)" "" 513 | debug "flow -n" 514 | assert_equal "${status}" 0 515 | end 516 | ####################################### 517 | : $((i++)) 518 | start "release rc2" 519 | take "${i}" 520 | { flow --init --yes \ 521 | && flow -y dev \ 522 | && flow -y dev 523 | } >/dev/null 524 | debug "flow -y" 525 | assert_equal "${status}" 0 526 | assert_equal "$(git show dev:VERSION)" "0.2.0" 527 | assert_equal "$(git show staging:VERSION)" "0.1.0" 528 | assert_equal "$(git show dev:CHANGELOG.md | head -7 | tail -1)" "## [Unreleased]" 529 | assert_contains "$(git show dev:CHANGELOG.md | head -9 | tail -1)" "## [0.1.0] - " 530 | assert_contains "$(git show dev:CHANGELOG.md | head -13 | tail -1)" "## [0.1.0-rc.2] - " 531 | assert_contains "$(git show dev:CHANGELOG.md | head -15 | tail -1)" "## [0.1.0-rc.1] - " 532 | assert_contains "$(git show main:CHANGELOG.md | head -7 | tail -1)" "## [0.1.0] - " 533 | assert_contains "$(git show main:CHANGELOG.md | head -11 | tail -1)" "## [0.1.0-rc.2] - " 534 | assert_contains "$(git show main:CHANGELOG.md | head -13 | tail -1)" "## [0.1.0-rc.1] - " 535 | assert_contains "$(git show-ref)" "refs/heads/dev" 536 | assert_equal "$(gsp)" "" 537 | debug "flow -n" 538 | assert_equal "${status}" 0 539 | end 540 | ####################################### 541 | # : $((i++)) 542 | # start "try create two stagings" 543 | # take "${i}" 544 | # { flow --init --yes \ 545 | # && echo a > a \ 546 | # && git add . \ 547 | # && git commit -m "add file a" \ 548 | # && flow --yes staging \ 549 | # && gch dev \ 550 | # && echo b > b \ 551 | # && git add . \ 552 | # && git commit -m "add file b" 553 | # } >/dev/null 2>&1 554 | # debug "flow -v --yes staging" 555 | # assert_equal "${status}" 0 556 | # assert_equal "$(git_current_branch)" "staging" 557 | # assert_equal "$(gsp)" "" 558 | # debug "flow -n" 559 | # assert_equal "${status}" 0 560 | # end 561 | ####################################### 562 | : $((i++)) 563 | start "force staging with uncommited changes" 564 | take "${i}" 565 | { flow --init --yes \ 566 | && echo a > a \ 567 | && git add . \ 568 | && git commit -m "add file a" \ 569 | && echo b > b 570 | } >/dev/null 571 | debug "flow -v --yes --force" 572 | assert_equal "${status}" 0 573 | assert_contains "${outlines[0]}" "Stashing files" 574 | assert_contains "$(git show-ref)" "refs/heads/staging" 575 | assert_equal "$(gsp | wc -l)" 1 576 | debug "flow -n" 577 | assert_equal "${status}" 5 578 | end 579 | ####################################### 580 | : $((i++)) 581 | start "merge staging" 582 | take "${i}" 583 | { flow --init --yes \ 584 | && echo a > a \ 585 | && git add . \ 586 | && git commit -m "add file a" \ 587 | && flow --yes 588 | } >/dev/null 589 | debug "flow -v --yes staging" 590 | assert_equal "${status}" 0 591 | assert_equal "$(git show dev:VERSION)" "0.2.0" 592 | assert_equal "$(git show staging:VERSION)" "0.1.0" 593 | assert_equal "$(git show main:VERSION)" "0.1.0" 594 | load_log 595 | assert_contains "${log}" "(HEAD -> dev) Merge branch 'main' into dev" 596 | assert_contains "${log}" "(tag: v0.1.0, staging, main-0, main) Merge branch 'staging'" 597 | assert_contains "${log}" "Update changelog and increment version" 598 | assert_equal "$(git show-ref | grep /refs/heads/staging)" "" 599 | assert_equal "$(gsp)" "" 600 | debug "flow -n" 601 | assert_equal "${status}" 0 602 | end 603 | ####################################### 604 | : $((i++)) 605 | start "invalid version number" 606 | take "${i}" 607 | { flow --init --yes \ 608 | && echo 0.0.0 > VERSION \ 609 | && git commit -am "version" 610 | } >/dev/null 2>&1 611 | debug "flow" 612 | assert_equal "${status}" 3 613 | assert_contains "${errlines[0]}" "Missing or invalid version file on dev." 614 | end 615 | ####################################### 616 | : $((i++)) 617 | start "invalid version number --conform" 618 | take "${i}" 619 | { flow --init --yes \ 620 | && gch staging \ 621 | && echo 1.0.0 > VERSION \ 622 | && git commit -am "version" 623 | } >/dev/null 2>&1 624 | debug "flow --conform --yes" 625 | assert_equal "${status}" 0 626 | assert_equal "$(cat VERSION)" "1.1.0" 627 | debug "flow -n" 628 | assert_equal "${status}" 0 629 | end 630 | ####################################### 631 | : $((i++)) 632 | start "unmerged changes on staging" 633 | take "${i}" 634 | { flow --init --yes \ 635 | && gch staging \ 636 | && touch a \ 637 | && git add . \ 638 | && git commit -am "a" 639 | } >/dev/null 640 | debug "flow --yes staging" 641 | assert_equal "${status}" 3 642 | assert_contains "${errlines[0]}" "Missing or invalid version file on staging." 643 | end 644 | ####################################### 645 | : $((i++)) 646 | start "unmerged changes on staging --conform" 647 | take "${i}" 648 | { flow --init --yes \ 649 | && gch staging \ 650 | && touch a \ 651 | && git add . \ 652 | && git commit -am "a" 653 | } >/dev/null 654 | debug "flow -cy staging" 655 | assert_equal "${status}" 0 656 | debug "flow -n" 657 | assert_equal "${status}" 0 658 | end 659 | ####################################### 660 | # : $((i++)) 661 | # start "merge staging with --request" 662 | # take "${i}" 663 | # { flow --init --yes \ 664 | # && flow --yes 665 | # } >/dev/null 666 | # debug "flow -v --yes --request" 667 | # fi 668 | ####################################### 669 | ## HOTFIX 670 | ####################################### 671 | : $((i++)) 672 | start "hotfix main" 673 | take "${i}" 674 | { flow -iy; } >/dev/null 675 | debug "flow -v --yes hotfix" 676 | assert_equal "${status}" 0 677 | assert_equal "$(git_current_branch)" "hotfix-$(whoami)" 678 | assert_equal "$(gsp)" "" 679 | debug "flow -n" 680 | assert_equal "${status}" 6 681 | end 682 | ####################################### 683 | # : $((i++)) 684 | # start "hotfix prod" 685 | # take "${i}" 686 | # { flow -iy; } >/dev/null 2>&1 687 | # debug "flow --yes hotfix myhotfix" 688 | # assert_equal "${status}" 0 689 | # assert_equal "$(git_current_branch)" "hotfix-myhotfix" 690 | # assert_equal "$(gsp)" "" 691 | # debug "flow -n" 692 | # assert_equal "${status}" 0 693 | # end 694 | ####################################### 695 | # : $((i++)) 696 | # start "hotfix stable by branch name" 697 | # take "${i}" 698 | # { flow -iy \ 699 | # && echo a > a \ 700 | # && git add . \ 701 | # && git commit -m "add file a" \ 702 | # && flow --yes staging \ 703 | # && flow --yes staging 704 | # } >/dev/null 2>&1 705 | # debug "flow -v --yes hotfix v0.0" 706 | # assert_equal "${status}" 0 707 | # load_log 708 | # assert_equal "$(git_current_branch)" "hotfix-${user}" 709 | # assert_equal "$(gsp)" "" 710 | # debug "flow -n" 711 | # assert_equal "${status}" 0 712 | # end 713 | ####################################### 714 | : $((i++)) 715 | start "merge main hotfix" 716 | take "${i}" 717 | { flow -iy \ 718 | && gch main \ 719 | && flow --yes myhotfix \ 720 | && echo a > a \ 721 | && git add . \ 722 | && git commit -m "add file a"; 723 | } >/dev/null 724 | debug "echo '' | flow --yes" 725 | assert_equal "${status}" 0 726 | load_log 727 | assert_contains "${loglines[0]}" "(HEAD -> dev) Merge branch 'main' into dev" 728 | assert_contains "${log}" "(tag: v0.0.1, staging, main-0, main) Merge branch 'myhotfix'" 729 | assert_equal "$(git show dev:VERSION)" "0.1.0" 730 | assert_equal "$(git show staging:VERSION)" "0.0.1" 731 | assert_equal "$(git show main:VERSION)" "0.0.1" 732 | assert_equal "$(git show-ref | grep /refs/heads/hotfix-myhotfix)" "" 733 | assert_equal "$(gsp)" "" 734 | debug "flow -n" 735 | assert_equal "${status}" 0 736 | end 737 | ####################################### 738 | : $((i++)) 739 | start "merge hotfix with unmerged staging" 740 | take "${i}" 741 | { flow -iy \ 742 | && flow --yes \ 743 | && flow --yes hotfix \ 744 | && echo b > b \ 745 | && git add . \ 746 | && git commit -m "add file b" 747 | } >/dev/null 748 | # assume conflicts 749 | debug "echo '' | flow --yes" 750 | assert_equal "${status}" 0 751 | load_log 752 | assert_contains "${loglines[0]}" "(HEAD -> dev) Merge branch 'staging' into dev" 753 | assert_contains "${log}" "(tag: v0.0.1, main-0, main) Merge branch 'hotfix-$(whoami)'" 754 | assert_equal "$(git show staging:b)" "b" 755 | assert_equal "$(git show dev:VERSION)" "0.2.0" 756 | assert_equal "$(git show staging:VERSION)" "0.1.0" 757 | assert_equal "$(git show main:VERSION)" "0.0.1" 758 | assert_equal "$(git show-ref | grep /refs/heads/hotfix-myhotfix)" "" 759 | assert_equal "$(gsp)" "" 760 | debug "flow -n" 761 | assert_equal "${status}" 0 762 | debug "flow -n" 763 | assert_equal "${status}" 0 764 | end 765 | ####################################### 766 | : $((i++)) 767 | start "merge staging into dev with conflicts" 768 | take "${i}" 769 | { 770 | flow -iy \ 771 | && flow --yes \ 772 | && git checkout dev \ 773 | && echo "dev content" > a \ 774 | && git add a \ 775 | && git commit -m "Update file 'a' in dev" \ 776 | && git checkout staging \ 777 | && echo "staging content" > a \ 778 | && git add a \ 779 | && git commit -m "Update file 'a' in staging" \ 780 | && git checkout dev 781 | } >/dev/null 2>&1 782 | debug "flow --yes --conform" 783 | assert_equal "${status}" 7 784 | assert_equal "${errlines[0]}" "flow: error: Committing is not possible because you have unmerged files." 785 | end 786 | -------------------------------------------------------------------------------- /flow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s extglob 4 | set -u 5 | 6 | LINES=${LINES:-$(tput lines)} 7 | COLUMNS=${COLUMNS:-$(tput cols)} 8 | 9 | DATAPATH=${DATAPATH:-$(dirname "${BASH_SOURCE[0]}")} 10 | USAGE=${USAGE:-} 11 | VERF=${VERF:-} 12 | 13 | ORIGIN=${FLOW_ORIGIN:-origin} 14 | UPSTREAM=${FLOW_UPSTREAM:-${ORIGIN}} 15 | CHANGELOG=${FLOW_CHANGELOG:-CHANGELOG.md} 16 | VERSION=${FLOW_VERSION:-VERSION} 17 | BCHD=${FLOW_BCHD:-dev} 18 | BCHS=${FLOW_BCHS:-staging} 19 | BCHP=${FLOW_BCHP:-main} 20 | 21 | main() { 22 | 23 | msg_start() { 24 | [[ ${VERBOSE} == 0 ]] \ 25 | && return 26 | MSGOPEN=1 27 | [[ "$(stdoutpipe)" || ${COLUMNS} -lt 41 ]] \ 28 | && echo -n "${1}" \ 29 | && return 30 | echo -n '[ ' 31 | save_cursor_position 32 | echo " .... ] ${1}" 33 | } 34 | 35 | msg_done() { 36 | msg_end "$(colorize ' done ' "${GREEN}")" 37 | } 38 | 39 | msg_pass() { 40 | msg_end "$(colorize ' pass ' "${BLUE}")" 41 | } 42 | 43 | msg_end() { 44 | [[ ${VERBOSE} == 0 || ${MSGOPEN} == 0 ]] \ 45 | && return 46 | MSGOPEN=0 47 | [[ "$(stdoutpipe)" || ${COLUMNS} -lt 41 ]] \ 48 | && echo " [ ${1} ]" \ 49 | && return 50 | set_cursor_position 51 | echo "${1}" 52 | } 53 | 54 | err() { 55 | echo "${SCRIPT_NAME}: ${1:-Generic error}" >&2 56 | [[ "${2:-}" == 2 ]] \ 57 | && echo "Run with --help to display usage." >&2 58 | [[ "${2:-}" == 3 ]] \ 59 | && echo "This appears to be fixable with the --conform option." >&2 60 | [[ "${2:-}" == 5 ]] \ 61 | && echo "Commit/stash your changes or use the --force option." >&2 62 | flow_finalize "${2:-1}" 63 | } 64 | 65 | setcolor() { 66 | local c=${1:-always} 67 | case ${c} in 68 | always|never|auto) 69 | COLOR=${c} 70 | return 0 71 | ;; 72 | esac 73 | err 'Unknown color value' 2 74 | } 75 | 76 | stdoutpipe() { 77 | readlink /proc/$$/fd/1 | grep -q '^pipe:' 78 | } 79 | 80 | colorize() { 81 | [[ ${COLOR} == never ]] \ 82 | && echo -n "${1}" \ 83 | && return 84 | [[ ${COLOR} == auto ]] \ 85 | && stdoutpipe \ 86 | && echo -n "${1}" \ 87 | && return 88 | tput setaf "${2:-${GREEN}}" 89 | echo -n "${1}" 90 | tput sgr0 91 | } 92 | 93 | clear_stdin() { 94 | while read -r -t 0; do read -r; done 95 | } 96 | 97 | save_cursor_position() { 98 | local curpos curecho curicanon curmin curtime 99 | curpos='1;1' 100 | exec < /dev/tty 101 | 102 | # Capture the specific settings we’re going to modify 103 | # Note: stty -a output formats vary; these extractions work with both GNU and uutils 104 | get_flag() { 105 | stty -a | grep -oE "\-?$1\b" 106 | } 107 | 108 | get_num() { 109 | stty -a | grep -oP "\b$1 = \K\d+" 110 | } 111 | 112 | curecho=$(get_flag 'echo') 113 | curicanon=$(get_flag 'icanon') 114 | curmin=$(get_num min) 115 | curtime=$(get_num time) 116 | 117 | # Normalize defaults if detection failed 118 | [[ -z "${curecho}" ]] && curecho='echo' 119 | [[ -z "${curicanon}" ]] && curicanon='icanon' 120 | [[ -z "${curmin}" ]] && curmin=1 121 | [[ -z "${curtime}" ]] && curtime=0 122 | 123 | stty -icanon -echo min 0 124 | 125 | echo -en '\033[6n' >/dev/tty 126 | # shellcheck disable=SC2162 127 | read -d'R' curpos /dev/null; then 165 | gsed "${@}" 166 | else 167 | sed "${@}" 168 | fi 169 | } 170 | 171 | git_status_empty() { 172 | [[ -z "$(git status --porcelain)" ]] 173 | } 174 | 175 | git_checkout() { 176 | local out 177 | out="$(git checkout "${@}" 2>&1)" \ 178 | || err "${out}" 179 | } 180 | 181 | git_reset() { 182 | local out 183 | out="$(git reset "${@}" 2>&1)" 184 | } 185 | 186 | git_log() { 187 | local out 188 | out="$(git log "${@}" 2>&1)" \ 189 | || err "${out}" 190 | echo "${out}" 191 | } 192 | 193 | git_remote_url() { 194 | git config remote."${ORIGIN}".url | flow_sed -e 's~^git@\([^:]\+\):~https://\1/~' -e 's/\.git$//' 195 | } 196 | 197 | diff_link() { 198 | local url 199 | url=$(git_remote_url) 200 | local from=${1} 201 | is_valid_version "${from}" \ 202 | && from="v${1}" 203 | local to=${2} 204 | is_valid_version "${to}" \ 205 | && to="v${2}" 206 | case "${url}" in 207 | *"${GITHUB}"*) echo "https://${url}/compare/${from}...${to}" ;; 208 | *"${GITLAB}"*) echo "https://${url}/compare/${from}...${to}" ;; 209 | *"${BITBUCKET}"*) echo "https://${url}/compare/${to}..${from}" ;; 210 | *) echo "git diff ${from}..${to}" ;; 211 | esac 212 | } 213 | 214 | abs_link() { 215 | local url 216 | url=$(git_remote_url) 217 | [[ -n "${url}" ]] \ 218 | && echo "${url}/releases/tag/v${1}" \ 219 | && return 220 | echo "git log v${1}" 221 | } 222 | 223 | link_esc() { 224 | [[ -n "${2}" ]] \ 225 | && flow_sed -e 's/[\/&]/\\&/g' <<< "$(diff_link "${1}" "${2}")" \ 226 | && return 227 | flow_sed -e 's/[\/&]/\\&/g' <<< "$(abs_link "${1}")" 228 | } 229 | 230 | git_init() { 231 | local out 232 | out="$(git init "${@}" 2>&1)" \ 233 | || err "${out}" 234 | } 235 | 236 | git_add() { 237 | local out 238 | out="$(git add "${@}" 2>&1)" \ 239 | || err "${out}" 240 | } 241 | 242 | git_commit() { 243 | local out 244 | out="$(git commit "${@}" 2>&1)" \ 245 | || err "${out}" 246 | } 247 | 248 | git_stash() { 249 | local out 250 | out="$(git stash "${@}" 2>&1)" \ 251 | || err "${out}" 252 | } 253 | 254 | git_fetch() { 255 | local out 256 | out="$(git fetch --update-head-ok "${@}" 2>&1)" \ 257 | || err "${out}" 258 | } 259 | 260 | git_tag() { 261 | local out 262 | out="$(git tag "${@}" 2>&1)" \ 263 | || err "${out}" >&2 264 | } 265 | 266 | top_vertag() { 267 | git tag --list | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" | sort -V | tail -1 | tr -d v 268 | } 269 | 270 | git_merge_nocommit() { 271 | env GIT_EDITOR=: git merge --no-ff --no-commit "${@}" >/dev/null 2>&1 272 | } 273 | 274 | git_merge() { 275 | local out 276 | out="$(env GIT_EDITOR=: git merge "${@}" 2>&1)" \ 277 | || err "${out}" 7 278 | } 279 | 280 | git_show() { 281 | local out 282 | out="$(git show "${@}" 2>&1)" \ 283 | || err "${out}" 284 | echo "${out}" 285 | } 286 | 287 | git_show_exists() { 288 | git show "${@}" >/dev/null 2>&1 289 | } 290 | 291 | latest_commit() { 292 | git rev-parse "${1}" 293 | } 294 | 295 | git_rev_list() { 296 | git rev-list -n1 "${1}" 297 | } 298 | 299 | git_branch() { 300 | local out 301 | out="$(git branch "${@}" 2>&1)" \ 302 | || err "${out}" 303 | } 304 | 305 | git_branch_format() { 306 | local out 307 | out="$(git branch --format='%(refname:short)' "${@}" 2>&1)" \ 308 | || err "${out}" 309 | echo "${out}" 310 | } 311 | 312 | git_push() { 313 | local out 314 | out="$(git push "${@}" 2>&1)" \ 315 | || err "${out}" 316 | } 317 | 318 | checkout_branch() { 319 | #msg_start "Creating branch '${1}' on '${2}'" 320 | local out 321 | out="$(git_checkout -b "${1}" "${2}")" \ 322 | || err "${out}" 323 | #msg_done 324 | } 325 | 326 | validate_branch() { 327 | branch_exists "${1}" \ 328 | && validate_behind "${1}" \ 329 | && return 330 | [[ ${CONFORM} == 0 ]] \ 331 | && err "Local branch '${1}' not found." 3 332 | confirm "Create branch '${1}'?" 333 | create_branch "${1}" 334 | [[ ${INIT} == 1 ]] \ 335 | && INITED=1 336 | } 337 | 338 | is_behind_origin() { 339 | git branch --verbose --list "${1}" | grep --quiet ' \[behind ' 340 | } 341 | 342 | validate_behind() { 343 | ! is_behind_origin "${1}" \ 344 | && return 345 | [[ ${CONFORM} == 0 ]] \ 346 | && err "Local branch '${1}' is behind remote." 3 347 | confirm "Merge branch '${1}' with its remote?" 348 | git fetch --update-head-ok "${ORIGIN}" "${1}:${1}" 349 | } 350 | 351 | create_branch() { 352 | branch_exists "${ORIGIN}/${1}" \ 353 | && git_branch "${1}" "${ORIGIN}/${1}" \ 354 | && return 355 | branch_exists "${BCHP}" \ 356 | && git_branch "${1}" "${BCHP}" \ 357 | && return 358 | git_branch "${1}" 359 | } 360 | 361 | branch_exists() { 362 | [[ $(current_branch) == "${1}" ]] \ 363 | || git_branch_format --all | grep -q -E "^${1}\$" 364 | } 365 | 366 | has_tag() { 367 | [[ -n $(git tag --merged "${1}" -l "${2}") ]] 368 | } 369 | 370 | git_repo_exists() { 371 | [[ -d .git ]] 372 | } 373 | 374 | validate_git_remote() { 375 | git config remote."${ORIGIN}".url >/dev/null \ 376 | || err "Remote url for '${ORIGIN}' not found." 377 | } 378 | 379 | current_branch() { 380 | git_branch_format --show-current 381 | } 382 | 383 | flow_finalize() { 384 | [[ "${INITED}" == 1 ]] \ 385 | && git_checkout "${BCHD}" 386 | [[ "${STASHED}" == 1 ]] \ 387 | && git_stash pop 388 | exit "${1:-0}" 389 | } 390 | 391 | is_merged_to() { 392 | git_branch_format --merged "${2}" | grep -q "^${1}$" 393 | } 394 | 395 | # merge upwards preserving conflicts 396 | # merge downwards ignoring VERSION and CHANGELOG conflicts 397 | # upwards: feature->dev->release->master, hotfix->master 398 | # downwards: master->release->dev 399 | merge_fromto() { 400 | local stream='upwards' 401 | [[ "${1}" == "${BCHP}" || "${2}" == "${BCHD}" ]] \ 402 | && stream='downwards' 403 | [[ "${1}" == "${BCHD}" || "${2}" == "${BCHS}" ]] \ 404 | && is_attached_to "${BCHD}" "${BCHS}" \ 405 | && stream='downwards' 406 | msg_start "Merging '${1}' to '${2}' (${stream})." 407 | git_checkout "${2}" 408 | is_merged_to "${1}" "${2}" \ 409 | && msg_pass \ 410 | && return 411 | git_merge_stream "${1}" "${stream}" 412 | msg_done 413 | } 414 | 415 | git_merge_stream() { 416 | [[ "${2}" == 'upwards' ]] \ 417 | && git_merge --no-ff "${1}" \ 418 | && return 419 | git_merge_nocommit "${1}" 420 | # hard reset version file regardless of a conflict 421 | git_reset -q "${VERSION}" 422 | git_checkout "${VERSION}" 423 | flow_sed -i '/^<<<<<<< /d;0,/^=======$/s///;/^=======$/d;/^>>>>>>> /d' "${CHANGELOG}" 424 | git_add "${CHANGELOG}" "${VERSION}" 425 | git_merge --continue 426 | } 427 | 428 | delete_remote_branch() { 429 | msg_start "Deleting remote branch '${1}'" 430 | ! branch_exists "${ORIGIN}/${1}" \ 431 | && msg_pass \ 432 | && return 433 | #TODO: used to be: 434 | #git push "${ORIGIN}" ":${REFSHEADS}/${1}" 2>&1 435 | git push -qd "${ORIGIN}" "${1}" 436 | msg_done 437 | } 438 | 439 | delete_branch() { 440 | [[ "$(current_branch)" == "${1}" ]] \ 441 | && err 'Unable to delete checked branch' 442 | delete_remote_branch "${1}" 443 | msg_start "Deleting local branch '${1}'" 444 | git_branch -d "${1}" 445 | msg_done 446 | } 447 | 448 | load_version() { 449 | local branch ver 450 | branch=${1:-$(current_branch)} 451 | git_show_exists "${branch}:${VERSION}" \ 452 | && ver=$(git_show "${branch}:${VERSION}") \ 453 | && is_valid_version "${ver}" \ 454 | && echo "${ver}" 455 | } 456 | 457 | is_valid_version() { 458 | [[ "${1}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] 459 | } 460 | 461 | parse_version() { 462 | echo "${1}" | flow_sed -n 's/.*\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/p' 463 | } 464 | 465 | version_gt() { 466 | ! version_lte "${1}" "${2}" 467 | } 468 | 469 | version_lte() { 470 | [[ "${1}" == "$(echo -e "${1}\n${2}" | sort -V | head -n1)" ]] 471 | } 472 | 473 | validate_requirements() { 474 | local bash_ver git_ver 475 | bash_ver="$(parse_version "$(bash --version 2>/dev/null)")" 476 | git_ver="$(parse_version "$(git --version 2>/dev/null)")" 477 | version_lte "${BASH_MINV}" "${bash_ver}" \ 478 | || err "Required bash version ${BASH_MINV} or later." 479 | version_lte "${GIT_MINV}" "${git_ver}" \ 480 | || err "Required git version ${GIT_MINV} or later." 481 | } 482 | 483 | # 1) validate repository existence 484 | # 2) validate repository consistency: 485 | # - at least one commit 486 | # - master branch 487 | validate_repository() { 488 | git_repo_exists \ 489 | && [[ -n "$(git_branch_format)" ]] \ 490 | && return 491 | [[ ${INIT} == 0 ]] \ 492 | && ! git_repo_exists \ 493 | && err 'Git repository not found.' 3 494 | [[ ${INIT} == 0 ]] \ 495 | && [[ -z "$(git_branch_format)" ]] \ 496 | && err 'Missing initial commit.' 3 497 | [[ "$(ls -A .)" && "${FORCE}" == 0 ]] \ 498 | && err 'Folder is not empty.' 5 499 | confirm "Initialize repository?" 500 | msg_start 'Initializing repository' 501 | # this requires newer git version 502 | #git_init --initial-branch="${BCHP}" 503 | ! git_repo_exists \ 504 | && git_init . \ 505 | && git symbolic-ref HEAD "refs/heads/${BCHP}" 506 | git_commit --allow-empty -m 'Initial commit' 507 | INITED=1 508 | msg_done 509 | } 510 | 511 | hotfix_vertag() { 512 | [[ ${CONFORM} == 0 ]] \ 513 | && err "Invalid version tag 'v${1}' commit." 3 514 | confirm 'Hotfix version tag?' 515 | #TODO hotfix 516 | local ver 517 | ver=$(increment_patch "${1}") 518 | update_version "${BCHP}" "${ver}" 519 | git_tag "v${ver}" "${BCHP}" 520 | } 521 | 522 | validate_tag() { 523 | has_tag "${BCHP}" "v${VERP}" \ 524 | && [[ $(git_rev_list "${REFSTAGS}/v${VERP}") == "$(latest_commit "${BCHP}")" ]] \ 525 | && return 526 | has_tag "${BCHP}" "v${VERP}" \ 527 | && hotfix_vertag "${VERP}" \ 528 | && return 529 | [[ ${CONFORM} == 0 ]] \ 530 | && err "Missing version tag 'v${VERP}'." 3 531 | confirm 'Create version tag?' 532 | git_tag "v${VERP}" "${BCHP}" 533 | } 534 | 535 | validate_merged_to() { 536 | is_merged_to "${1}" "${2}" \ 537 | && return 538 | [[ ${CONFORM} == 0 ]] \ 539 | && err "Branch '${1}' is not merged into '${2}'." 3 540 | confirm "Merge branch '${1}' into '${2}'?" 541 | merge_fromto "${1}" "${2}" 542 | } 543 | 544 | is_attached_to() { 545 | [[ $(git_rev_list "${REFSHEADS}/${1}") == $(git_rev_list "${REFSHEADS}/${2}") ]] 546 | } 547 | 548 | update_version() { 549 | local gcb 550 | gcb=$(current_branch) 551 | msg_start 'Updating version number' 552 | git_checkout "${1}" 553 | echo "${2}" > "${VERSION}" 554 | git_add "${VERSION}" 555 | git_commit -am 'Update version number' 556 | msg_done 557 | git_checkout "${gcb}" 558 | } 559 | 560 | # validate git status 561 | validate_status_empty() { 562 | [[ ${FORCE} == 1 ]] \ 563 | && msg_start 'Stashing files' 564 | git_status_empty \ 565 | && msg_pass \ 566 | && return 567 | [[ ${FORCE} == 0 ]] \ 568 | && err 'Uncommitted changes.' 5 569 | git_add -A 570 | git_stash push 571 | msg_done 572 | STASHED=1 573 | } 574 | 575 | default_changelog() { 576 | local url 577 | url=$(git_remote_url) 578 | echo '# Change Log' 579 | echo 'All notable changes to this project will be documented in this file.' 580 | echo 581 | echo 'The format is based on [Keep a Changelog](http://keepachangelog.com/)' 582 | echo 'and this project adheres to [Semantic Versioning](http://semver.org/).' 583 | echo 584 | echo "## [${1}] - $(date "${DATE_FORMAT}")" 585 | echo 586 | echo "### Added" 587 | echo 588 | echo "- New changelog file." 589 | echo 590 | echo "[${1}]: $(abs_link "${1}")" 591 | } 592 | 593 | # 1: source branch, empty for default changelog 594 | restore_changelog() { 595 | [[ -z "${1:-}" ]] \ 596 | && default_changelog "$(load_version)" > "${CHANGELOG}" \ 597 | && return 598 | git_show "${1}:${CHANGELOG}" > "${CHANGELOG}" 599 | } 600 | 601 | # 1: branch 602 | # 2: restore from branch, empty for create new 603 | # 3: head stamp version or 'unreleased' 604 | # 4: foot stamp from or empty 605 | # 5: foot stamp to or empty 606 | validate_changelog() { 607 | git_show_exists "${1}:${CHANGELOG}" \ 608 | && [[ -n $(git_show "${1}:${CHANGELOG}") ]] \ 609 | && git_show "${1}:${CHANGELOG}" | grep -q "^## \[${3//./\.}]" \ 610 | && git_show "${1}:${CHANGELOG}" | grep -q "^\[${3//./\.}]" \ 611 | && return 612 | [[ ${CONFORM} == 0 ]] \ 613 | && err "Missing or invalid changelog file on ${1}." 3 614 | confirm "Conform changelog file on ${1}?" 615 | local gcb 616 | gcb=$(current_branch) 617 | git_checkout "${1}" 618 | # changelog must exist and must be unempty 619 | [[ ! -f "${CHANGELOG}" || ! -s "${CHANGELOG}" ]] \ 620 | && restore_changelog "${2}" 621 | # changelog must contain some heading and diff stamp 622 | # shellcheck disable=SC2015 623 | grep -q '^## ' "${CHANGELOG}" \ 624 | && grep -q '^\[' "${CHANGELOG}" \ 625 | || restore_changelog "${2}" 626 | # shellcheck disable=SC2015 627 | ! grep -q "^## \[${3//./\.}]" "${CHANGELOG}" \ 628 | && changelog_head "${3}" 629 | ! grep -q "^\[${3//./\.}]" "${CHANGELOG}" \ 630 | && changelog_foot "${3}" "${4:-${3}}" "${5:-}" 631 | git_add -- "${CHANGELOG}" 632 | git_commit -am "Conform changelog file on '${1}'" 633 | git_checkout "${gcb}" 634 | } 635 | 636 | # 1: branch 637 | # 2: min version 638 | validate_version() { 639 | local ver 640 | ver=$(load_version "${1}") 641 | [[ -n "${ver}" ]] \ 642 | && ! version_gt "${2:-0.0.0}" "${ver}" \ 643 | && return 644 | [[ ${CONFORM} == 0 ]] \ 645 | && err "Missing or invalid version file on ${1}." 3 646 | confirm "Conform version file on ${1}?" 647 | local gcb 648 | gcb=$(current_branch) 649 | git_checkout "${1}" 650 | echo "${2:-0.0.0}" > "${VERSION}" 651 | git_add "${VERSION}" 652 | git_commit -am "Conform version file on ${1}" 653 | git_checkout "${gcb}" 654 | } 655 | 656 | find_or_first() { 657 | for branch in "${@}"; do 658 | branch_exists "${branch}" \ 659 | && echo "${branch}" \ 660 | && return 661 | done 662 | for branch in "${@}"; do 663 | branch_exists "${ORIGIN}/${branch}" \ 664 | && echo "${branch}" \ 665 | && return 666 | done 667 | echo "${1}" 668 | } 669 | 670 | flow_validate() { 671 | local gcb 672 | gcb=$(current_branch) 673 | BCHP=$(find_or_first "${BCHP}" "${ARRP[@]}") 674 | BCHS=$(find_or_first "${BCHS}" "${ARRS[@]}") 675 | BCHD=$(find_or_first "${BCHD}" "${ARRD[@]}") 676 | validate_branch "${BCHP}" 677 | validate_version "${BCHP}" "$(top_vertag)" 678 | VERP=$(load_version "${BCHP}") 679 | validate_changelog "${BCHP}" "" "${VERP}" 680 | validate_tag 681 | validate_prod_branch 682 | validate_branch "${BCHS}" 683 | validate_branch "${BCHD}" 684 | validate_merged_to "${BCHP}" "${BCHD}" 685 | VERS=${VERP} 686 | ! is_attached_to "${BCHP}" "${BCHS}" \ 687 | && validate_merged_to "${BCHP}" "${BCHS}" \ 688 | && validate_version "${BCHS}" "$(increment_minor "${VERP}")" \ 689 | && VERS=$(load_version "${BCHS}") \ 690 | && validate_changelog "${BCHS}" "${BCHP}" "${VERS}-rc.1" "${VERP}" 691 | validate_merged_to "${BCHS}" "${BCHD}" 692 | validate_version "${BCHD}" "$(increment_minor "${VERS}")" 693 | VERD=$(load_version "${BCHD}") 694 | validate_changelog "${BCHD}" "${BCHS}" "${UNRELEASED}" "${BCHS}" "${BCHD}" 695 | validate_param "${1:-}" 696 | validate_param "${DEFAULT_HOTFIX}" 697 | validate_param "${DEFAULT_FEATURE}" 698 | git_checkout "${gcb}" 699 | } 700 | 701 | is_prod_branch() { 702 | [[ "${1}" =~ ^${BCHP}-[0-9]+$ ]] 703 | } 704 | 705 | # prod* must exist 706 | # else optional 707 | validate_param() { 708 | [[ -z "${1}" ]] \ 709 | && return 710 | branch_exists "${ORIGIN}/${1}" \ 711 | && ! branch_exists "${1}" \ 712 | && git_branch "${1}" "${ORIGIN}/${1}" 713 | ! branch_exists "${1}" \ 714 | && ! is_prod_branch "${1}" \ 715 | && return 716 | ! branch_exists "${1}" \ 717 | && err "Expected branch '${1}' not found." 4 718 | validate_behind "${1}" 719 | } 720 | 721 | validate_prod_branch() { 722 | local prod="${BCHP}-${VERP%%.*}" 723 | validate_branch "${prod}" 724 | is_attached_to "${prod}" "${BCHP}" \ 725 | && return 726 | [[ ${CONFORM} == 0 ]] \ 727 | && err "Branch '${prod}' is behind '${BCHP}'." 3 728 | confirm "Merge branch '${prod}' into '${BCHP}'?" 729 | git_checkout "${prod}" 730 | git_merge "${BCHP}" 731 | } 732 | 733 | get_key_branch() { 734 | [[ " ${ARRP[*]} " == *" ${1} "* ]] \ 735 | && echo "${BCHP}" \ 736 | && return 737 | [[ " ${ARRS[*]} " == *" ${1} "* ]] \ 738 | && echo "${BCHS}" \ 739 | && return 740 | [[ " ${ARRD[*]} " == *" ${1} "* ]] \ 741 | && echo "${BCHD}" \ 742 | && return 743 | echo "${1}" 744 | } 745 | 746 | is_hotfix_branch() { 747 | [[ "$(git merge-base "${1}" "${BCHD}")" == "$(git merge-base "${BCHP}" "${BCHD}")" ]] 748 | } 749 | 750 | # 1: dest branch 751 | # 2: source branch or empty 752 | flow_hotfix() { 753 | branch_exists "${1}" \ 754 | && release_hotfix "${1}" \ 755 | && return 756 | local branch="${BCHP}" 757 | [[ "$(is_prod_branch "$(current_branch)")" ]] \ 758 | && branch=$(current_branch) 759 | [[ -n "${2:-}" ]] \ 760 | && branch="${2}" 761 | create_hotfix "${1}" "${branch}" 762 | } 763 | 764 | # 1: dest branch 765 | flow_feature() { 766 | branch_exists "${1}" \ 767 | && release_feature "${1}" \ 768 | && return 769 | create_feature "${1}" 770 | } 771 | 772 | flow_action() { 773 | local branch 774 | branch=$(get_key_branch "${1:-"$(current_branch)"}") 775 | # shellcheck disable=SC2015 776 | git check-ref-format "${REFSHEADS}/${branch}" \ 777 | && [[ "${branch,,}" != "head" ]] \ 778 | || err "Invalid branch name." 2 779 | # flow dev* -> release dev 780 | [[ "${branch}" == "${BCHD}" ]] \ 781 | && release_dev \ 782 | && return 783 | # flow staging* -> release staging 784 | [[ "${branch}" == "${BCHS}" ]] \ 785 | && release_staging \ 786 | && return 787 | # flow stable* -> create/release default hotfix 788 | [[ "${branch}" == "${BCHP}" ]] \ 789 | && flow_hotfix "${DEFAULT_HOTFIX}" "${BCHP}" \ 790 | && return 791 | # flow prod* -> create/release default hotfix on a prod* branch 792 | is_prod_branch "${branch}" \ 793 | && flow_hotfix "${DEFAULT_HOTFIX}" "${branch}" \ 794 | && return 795 | # flow hotfix -> create/release default hotfix on a stable or prod* branch 796 | [[ "${branch}" == "${HOTFIX}" ]] \ 797 | && flow_hotfix "${DEFAULT_HOTFIX}" \ 798 | && return 799 | # flow feature -> create/release default feature 800 | [[ "${branch}" == "${FEATURE}" ]] \ 801 | && flow_feature "${DEFAULT_FEATURE}" \ 802 | && return 803 | # flow other existing hotfix branch -> release hotfix 804 | branch_exists "${branch}" \ 805 | && is_hotfix_branch "${branch}" \ 806 | && release_hotfix "${branch}" \ 807 | && return 808 | # if other existing branch -> release feature 809 | branch_exists "${branch}" \ 810 | && release_feature "${branch}" \ 811 | && return 812 | # if on stable|prod -> create hotfix branch 813 | is_hotfix_branch "$(current_branch)" \ 814 | && create_hotfix "${1}" "$(current_branch)" \ 815 | && return 816 | # create feature branch 817 | create_feature "${1}" 818 | } 819 | 820 | release_dev() { 821 | is_attached_to "${BCHS}" "${BCHP}" \ 822 | && create_staging \ 823 | && return 824 | increment_staging 825 | } 826 | 827 | create_staging() { 828 | confirm "* Create new release candidate from '${BCHD}'?" 829 | msg_start 'Creating new release candidate' 830 | merge_fromto "${BCHD}" "${BCHS}" 831 | changelog_head "${VERD}-rc.1" 832 | changelog_foot "${VERD}-rc.1" "${VERP}" 833 | git_add . 834 | git_commit -am 'Update changelog' 835 | msg_done 836 | merge_fromto "${BCHS}" "${BCHD}" 837 | increment_minor "${VERD}" > "${VERSION}" 838 | changelog_head "${UNRELEASED}" 839 | changelog_foot "${UNRELEASED}" "${BCHS}" "${BCHD}" 840 | git_commit -am "Update changelog and increment version" 841 | git_checkout "${BCHS}" 842 | } 843 | 844 | increment_staging() { 845 | confirm "* Increment release candidate from '${BCHD}'?" 846 | msg_start 'Incrementing release candidate' 847 | merge_fromto "${BCHD}" "${BCHS}" 848 | local nthrc=$(( $(grep -c "^## \[${VERS//./\.}-rc\." "${CHANGELOG}") + 1 )) 849 | changelog_head "${VERS}-rc.${nthrc}" 850 | changelog_foot "${VERS}-rc.${nthrc}" "${VERP}" 851 | echo "${VERS}" > "${VERSION}" 852 | git_add . 853 | git_commit -am 'Update changelog and restore version' 854 | msg_done 855 | merge_fromto "${BCHS}" "${BCHD}" 856 | changelog_head "${UNRELEASED}" 857 | changelog_foot "${UNRELEASED}" "${BCHS}" "${BCHD}" 858 | git_commit -am "Update changelog" 859 | git_checkout "${BCHS}" 860 | } 861 | 862 | create_feature() { 863 | confirm "* Create feature '${1}' from '${BCHD}'?" 864 | checkout_branch "${1}" "${BCHD}" 865 | } 866 | 867 | # 1 dest name 868 | # 2 source branch 869 | create_hotfix() { 870 | confirm "* Create hotfix '${1}' from '${2}'?" 871 | checkout_branch "${1}" "${2}" 872 | } 873 | 874 | release_staging() { 875 | # shellcheck disable=SC2015 876 | [[ ${REQUEST} == 1 ]] \ 877 | && confirm "* Request merge '${BCHS}' to '${BCHP}'?" \ 878 | || confirm "* Release '${BCHS}' branch to '${BCHP}'?" 879 | is_attached_to "${BCHS}" "${BCHP}" \ 880 | && err 'Staging branch is already released.' 6 881 | merge_fromto "${BCHP}" "${BCHS}" 882 | msg_start 'Updating changelog' 883 | local note 884 | note="Stable release based on $(grep -om1 "^\[[^\]\+]" "${CHANGELOG}")." 885 | changelog_head "${VERS}" "${note}" 886 | changelog_foot "${VERS}" "${VERP}" "${VERS}" 887 | git_add -- "${CHANGELOG}" 888 | git_commit -am 'Update changelog' 889 | msg_done 890 | [[ ${REQUEST} == 1 ]] \ 891 | && flow_request "${BCHP}" \ 892 | && return 893 | merge_fromto "${BCHS}" "${BCHP}" 894 | msg_start 'Updating branching structure' 895 | git_tag "v${VERS}" 896 | git_checkout "${BCHS}" 897 | git_merge "${BCHP}" 898 | local prod="${BCHP}-${VERS%%.*}" 899 | # shellcheck disable=SC2015 900 | branch_exists "${prod}" \ 901 | && git_checkout "${prod}" \ 902 | && git_merge "${BCHP}" \ 903 | || git_branch "${prod}" 904 | msg_done 905 | merge_fromto "${BCHP}" "${BCHD}" 906 | } 907 | 908 | # 1: stamp version or empty for unreleased 909 | # 2: optional note 910 | changelog_head() { 911 | local head_stamp="## [${1}]" 912 | [[ "${1}" != "${UNRELEASED}" ]] \ 913 | && head_stamp="## [${1}] - $(date "${DATE_FORMAT}")" 914 | # always add head stamp or replace the unreleased one 915 | # shellcheck disable=SC2015 916 | grep -q "^## \[${UNRELEASED}]" "${CHANGELOG}" \ 917 | && flow_sed -i "s/^## \[${UNRELEASED}].*/${head_stamp}/" "${CHANGELOG}" \ 918 | || flow_sed -i "0,/^## /s//${head_stamp%* - }\n\n&/" "${CHANGELOG}" 919 | [[ -n "${2:-}" ]] \ 920 | && flow_sed -i "0,/^## \[.*/s//\0\n\n_${2}_/" "${CHANGELOG}" 921 | } 922 | 923 | # 1: stamp 924 | # 2: from or at (if to is empty) 925 | # 3: to or empty 926 | changelog_foot() { 927 | local stamp 928 | stamp="[${1}]: $(link_esc "${2}" "${3:-}")" 929 | # update diff stamp if exists 930 | grep -q "^\[${1//./\.}]" "${CHANGELOG}" \ 931 | && flow_sed -i "s/^\[${1//./\.}].*/${stamp}/" "${CHANGELOG}" \ 932 | && return 933 | # replace unreleased if exists 934 | grep -q "^\[${UNRELEASED}]" "${CHANGELOG}" \ 935 | && flow_sed -i "s/^\[${UNRELEASED}].*/${stamp}/" "${CHANGELOG}" \ 936 | && return 937 | # else add diff stamp 938 | flow_sed -i "0,/^\[/s//${stamp}\n&/" "${CHANGELOG}" 939 | } 940 | 941 | # 1: keyword 942 | # 2: message 943 | add_changelog_entry() { 944 | local tmpfile 945 | tmpfile="$(mktemp)" 946 | gawk -v kw="${1}" -v msg="${2}" -v kws="${KEYWORDS[*]#*${1}}" ' 947 | function add_entry () { 948 | print "### " kw 949 | print "" 950 | print "- " msg 951 | print "" 952 | } 953 | BEGIN { 954 | before = 1 955 | after = 0 956 | } 957 | /^$/ { 958 | print 959 | next 960 | } 961 | /^## \[/ && before == 1 && after == 0 { 962 | before = 0 963 | print 964 | next 965 | } 966 | /^## \[/ && before == 0 { 967 | if (after == 0) { 968 | add_entry() 969 | } 970 | after = 1 971 | print 972 | next 973 | } 974 | before == 1 || after == 1 { 975 | print 976 | next 977 | } 978 | $0 ~ "^### " kw { 979 | print 980 | getline 981 | print 982 | print "- " msg 983 | after = 1 984 | next 985 | } 986 | $0 ~ "^### " kws { 987 | add_entry() 988 | print 989 | after = 1 990 | next 991 | } 992 | { 993 | print 994 | } 995 | ' "${CHANGELOG}" > "${tmpfile}" 996 | mv "${tmpfile}" "${CHANGELOG}" 997 | } 998 | 999 | # 1: default keyword 1000 | # 2: dest branch 1001 | update_changelog() { 1002 | local gcb def kw= 1003 | gcb=$(current_branch) 1004 | def="New feature '${gcb}'." 1005 | [[ "${ENTRY}" == 1 ]] \ 1006 | && add_changelog_entry "${1}" "${def}" \ 1007 | && return 1008 | echo 1009 | echo '###' 1010 | echo "# Enter '${gcb}' description for ${CHANGELOG}" 1011 | echo '# New line for multiple entries.' 1012 | echo '# Empty message to skip or end editing.' 1013 | echo '#' 1014 | echo '# Format' 1015 | echo "# 'keyword: message'" 1016 | echo '#' 1017 | echo '# Available keywords' 1018 | echo "# ${KEYWORDS[*]}" 1019 | echo '#' 1020 | echo "# Branch '${gcb}' commits" 1021 | git_log "${2}".."${gcb}" --pretty=format:'# %h %s' 1022 | echo '###' 1023 | echo 1024 | # What for? 1025 | REPLY= 1026 | while read -eri "${1}: " line; do 1027 | [[ -z "${line#*:}" ]] \ 1028 | && break 1029 | history -s "${line}" 1030 | kw="${line%%: *}" 1031 | [[ " ${KEYWORDS[*]} " != *" ${kw} "* ]] \ 1032 | && kw=${1} 1033 | #add_changelog_entry "${kw}" "$(echo "${line#*:}" | xargs)" 1034 | add_changelog_entry "${kw}" "${line#*: }" 1035 | done 1036 | [[ -z "${kw}" ]] \ 1037 | && add_changelog_entry "${1}" "${def}" 1038 | } 1039 | 1040 | release_feature() { 1041 | is_merged_to "${1}" "${BCHD}" \ 1042 | && err 'Nothing to merge.' 6 1043 | # shellcheck disable=SC2015 1044 | [[ ${REQUEST} == 1 ]] \ 1045 | && confirm "* Request merge '${1}' into '${BCHD}'?" \ 1046 | || confirm "* Merge feature '${1}' into '${BCHD}'?" 1047 | merge_fromto "${BCHD}" "${1}" 1048 | update_changelog Added "${BCHD}" 1049 | git_commit -am 'Update changelog' 1050 | [[ ${REQUEST} == 1 ]] \ 1051 | && flow_request "${BCHD}" \ 1052 | && return 1053 | merge_fromto "${1}" "${BCHD}" 1054 | delete_branch "${1}" 1055 | } 1056 | 1057 | # 1: version 1058 | # 2: major|minor|patch 1059 | increment_version() { 1060 | local major minor patch 1061 | major=$(echo "${1}" | cut -d. -f1) 1062 | minor=$(echo "${1}" | cut -d. -f2) 1063 | patch=$(echo "${1}" | cut -d. -f3) 1064 | case "${2:-}" in 1065 | major) 1066 | (( major ++ )) 1067 | minor=0 1068 | patch=0 1069 | ;; 1070 | minor) 1071 | (( minor ++ )) 1072 | patch=0 1073 | ;; 1074 | patch) 1075 | (( patch ++ )) 1076 | ;; 1077 | *) 1078 | err 'Version increment parameter missing or invalid.' 1079 | esac 1080 | echo "${major}.${minor}.${patch}" 1081 | } 1082 | 1083 | increment_patch() { 1084 | increment_version "${1}" patch 1085 | } 1086 | 1087 | increment_minor() { 1088 | increment_version "${1}" minor 1089 | } 1090 | 1091 | release_hotfix() { 1092 | local verh 1093 | verh=$(load_version "${1}") 1094 | # shellcheck disable=SC2015 1095 | [[ -n "${verh}" ]] \ 1096 | && [[ ! "${verh%%.*}" > "${VERP%%.*}" ]] \ 1097 | || err "Invalid hotfix version." 4 1098 | local dest=${BCHP}-${verh%%.*} 1099 | ! branch_exists "${dest}" \ 1100 | && err "Branch '${dest}' not found." 4 1101 | is_merged_to "${1}" "${dest}" \ 1102 | && err 'Nothing to merge.' 6 1103 | # shellcheck disable=SC2015 1104 | [[ "${verh%%.*}" == "${VERP%%.*}" ]] \ 1105 | && confirm "* Merge hotfix '${1}' into '${dest}' and '${BCHP}'?" \ 1106 | || confirm "* Merge hotfix '${1}' only into '${dest}'?" 1107 | msg_start 'Updating control files' 1108 | git_checkout "${1}" 1109 | local verd 1110 | verd=$(load_version "${dest}") 1111 | verh=$(increment_patch "${verd}") 1112 | echo "${verh}" > "${VERSION}" 1113 | changelog_head "${verh}" 1114 | changelog_foot "${verh}" "${verd}" "${verh}" 1115 | update_changelog Fixed "${dest}" 1116 | git_commit -am 'Update control files' 1117 | msg_done 1118 | [[ ${REQUEST} == 1 ]] \ 1119 | && flow_request "${dest}" \ 1120 | && return 1121 | merge_fromto "${1}" "${dest}" 1122 | git_tag "v${verh}" 1123 | [[ "${verh%%.*}" != "${VERP%%.*}" ]] \ 1124 | && delete_branch "${1}" \ 1125 | && return 1126 | git_checkout "${BCHP}" 1127 | git_merge "${dest}" 1128 | [[ "${VERP}" == "${VERS}" ]] \ 1129 | && git_checkout "${BCHS}" \ 1130 | && git_merge "${BCHP}" \ 1131 | && merge_fromto "${BCHP}" "${BCHD}" \ 1132 | && delete_branch "${1}" \ 1133 | && return 1134 | merge_fromto "${BCHP}" "${BCHS}" 1135 | merge_fromto "${BCHS}" "${BCHD}" 1136 | delete_branch "${1}" 1137 | git_checkout "${BCHD}" 1138 | } 1139 | 1140 | flow_pull() { 1141 | validate_git_remote 1142 | confirm '* Pull all remote branches?' 1143 | msg_start 'Pulling branches' 1144 | local gcb 1145 | gcb=$(current_branch) 1146 | git fetch --update-head-ok --all --prune 1147 | git fetch --tags 1148 | for branch in $(git_branch_format -r | grep "^${ORIGIN}" | flow_sed "s/^${ORIGIN}\///"); do 1149 | [[ "${branch}" == HEAD ]] \ 1150 | && continue 1151 | [[ "${branch}" == "${ORIGIN}" ]] \ 1152 | && continue 1153 | git_checkout "${branch}" 1154 | is_behind_origin "${branch}" \ 1155 | && git_merge --ff-only "${ORIGIN}/${branch}" 1156 | git_branch --set-upstream-to "${ORIGIN}/${branch}" 1157 | done 1158 | git_checkout "${gcb}" 1159 | msg_done 1160 | } 1161 | 1162 | flow_push() { 1163 | validate_git_remote 1164 | confirm '* Push all branches to the remote repository?' 1165 | msg_start 'Pushing all branches and tags' 1166 | git push "${ORIGIN}" --all 1167 | git push "${ORIGIN}" --tags 1168 | msg_done 1169 | } 1170 | 1171 | flow_request() { 1172 | validate_git_remote 1173 | local gcb 1174 | gcb=$(current_branch) 1175 | git push "${ORIGIN}" "${1}" 1176 | git push "${ORIGIN}" "${gcb}" 1177 | request_url_fromto "${gcb}" "${1}" 1178 | } 1179 | 1180 | trim_url() { 1181 | local url 1182 | url="${1#https://}" 1183 | echo "${url}" | grep -q ":" \ 1184 | && url="${url#*@}" \ 1185 | && url="${url/://}" \ 1186 | && url="${url/.git/}" 1187 | echo "${url}" 1188 | } 1189 | 1190 | # 1: from 1191 | # 2: to 1192 | request_url_fromto() { 1193 | local url upstream_url 1194 | url=$(trim_url "$(git config remote."${ORIGIN}".url)") 1195 | upstream_url="$(trim_url "$(git config remote."${UPSTREAM}".url)")" 1196 | echo -n 'Pull request URL: ' 1197 | case "${url}" in 1198 | *"${BITBUCKET}"*) 1199 | # shellcheck disable=SC1003 1200 | [[ "${url}" == "${upstream_url}" ]] \ 1201 | && echo "https://${url}/compare/${1}..$(echo "${url}" | cut -d'/' -f2-3)%3A${2}" \ 1202 | || echo "https://${url}/compare/${1}..$(echo "${upstream_url}" | cut -d'/' -f2-3)%3A${2}" 1203 | ;; 1204 | *"${GITHUB}"*) 1205 | # shellcheck disable=SC1003 1206 | [[ "${url}" == "${upstream_url}" ]] \ 1207 | && echo "https://${url}/compare/${2}...${1}?expand=1" \ 1208 | || echo "https://${upstream_url}/compare/${2}...$(echo "${url}" | cut -d'/' -f2)%3A${1}?expand=1" 1209 | ;; 1210 | *"$GITLAB"*) 1211 | # shellcheck disable=SC1003 1212 | [[ "${url}" == "${upstream_url}" ]] \ 1213 | && echo "https://${url}/compare/${2}...${1}" \ 1214 | || echo "https://${upstream_url}/compare/${2}...$(echo "${url}" | cut -d'/' -f2)" 1215 | ;; 1216 | *) 1217 | err 'Unsupported remote server name.' 1218 | ;; 1219 | esac 1220 | } 1221 | 1222 | flow_usage() { 1223 | local file head tail 1224 | head="$(echo "${USAGE:-}" | head -n1)" 1225 | tail="$(echo "${USAGE:-}" | tail -n+2)" 1226 | file="${DATAPATH}/${SCRIPT_NAME}.usage" 1227 | [[ -z "${head}" ]] \ 1228 | && [[ ! -f "${file}" ]] \ 1229 | && echo 'Usage is not available in source file.' \ 1230 | && return 1231 | [[ -z "${head}" ]] \ 1232 | && head="$(head -n1 "${file}")" \ 1233 | && tail="$(tail -n+2 "${file}")" 1234 | echo "${head}" 1235 | echo 1236 | local indent=0 1237 | [[ ${COLUMNS} -gt 1 ]] \ 1238 | && indent=5 \ 1239 | && export MANWIDTH=$(( COLUMNS + indent )) 1240 | echo "${tail}" | man --nj --nh -l - 2>/dev/null \ 1241 | | flow_sed "1,2d;/^[[:space:]]*$/d;\${d};s/^ \{${indent}\}//" 1242 | } 1243 | 1244 | flow_version() { 1245 | [[ -n "${VERF}" ]] \ 1246 | && echo "flow ${VERF}" \ 1247 | && return 1248 | echo "flow $(<"${DATAPATH}/VERSION")" 1249 | } 1250 | 1251 | flow_whatnow() { 1252 | local branch 1253 | branch=$(current_branch) 1254 | [[ -z "${branch}" ]] \ 1255 | && err 'Detached branch.' 6 1256 | echo '***' 1257 | echo "* Flow on '${branch}'" 1258 | echo '*' 1259 | branch_desc "${branch}" 1260 | echo '***' 1261 | } 1262 | 1263 | branch_desc() { 1264 | [[ "${1}" == "${BCHP}" || "$(is_prod_branch "${1}")" ]] \ 1265 | && echo '* This is considered a read-only stable branch.' \ 1266 | && echo '* Do not commit any changes directly to this branch ever!' \ 1267 | && echo '*' \ 1268 | && echo "* 1. Run '${SCRIPT_NAME}' to create a hotfix or leave." \ 1269 | && return 1270 | [[ "${1}" == "${BCHS}" ]] \ 1271 | && is_attached_to "${BCHS}" "${BCHP}" \ 1272 | && echo '* This is a staging branch that is already released.' \ 1273 | && echo '* Nothing to do until a new version is released from dev.' \ 1274 | && echo '*' \ 1275 | && echo "* 1. Leave this branch." \ 1276 | && return 1277 | [[ "${1}" == "${BCHS}" ]] \ 1278 | && echo '* This branch is meant solely for bug fixing.' \ 1279 | && echo '* Each commit must be merged into the development branch.' \ 1280 | && echo '*' \ 1281 | && echo '* 1. Make some fixes and feel free to commit directly.' \ 1282 | && echo "* 2. Run '${SCRIPT_NAME}' to release this branch." \ 1283 | && return 1284 | [[ "${1}" == "${BCHD}" ]] \ 1285 | && echo '* This is a development branch.' \ 1286 | && echo '* It is designed for bug fixing and merging features.' \ 1287 | && echo '*' \ 1288 | && echo '* 1. Make some fixes and feel free to commit directly.' \ 1289 | && echo "* 2. Run '${SCRIPT_NAME} feature_name' to create a new feature." \ 1290 | && echo "* 3. Run '${SCRIPT_NAME}' to release this branch." \ 1291 | && return 1292 | is_hotfix_branch "${1}" \ 1293 | && echo '* This is a temporary hotfix branch created from a stable branch.' \ 1294 | && echo '* Its purpose is to fix one critical problem and dissolve.' \ 1295 | && echo '* WARNING: Make only minimum necessary changes here!' \ 1296 | && echo '*' \ 1297 | && echo '* 1. Make sure you really HAVE to hotfix the problem.' \ 1298 | && echo '* 2. Fix the critical problem and commit changes.' \ 1299 | && echo "* 3. Run '${SCRIPT_NAME}' to merge the hotfix back into stable branch." \ 1300 | && echo '* 4. Good luck.' \ 1301 | && return 1302 | echo '* This is a temporary feature branch.' 1303 | echo '* Its purpose is to create a (one) new feature.' 1304 | echo '*' 1305 | echo '* 1. Create and develop the feature. One feature is a perfect amount.' 1306 | echo "* 2. Run '${SCRIPT_NAME}' to merge it back into dev." 1307 | } 1308 | 1309 | # global constants 1310 | declare -r BASH_MINV=3.2 1311 | declare -r GIT_MINV=1.8.0 1312 | declare -r SCRIPT_NAME=${0##*/} 1313 | declare -r GREEN=2 1314 | declare -r BLUE=4 1315 | declare -r BITBUCKET='bitbucket.org' 1316 | declare -r GITHUB='github.com' 1317 | declare -r GITLAB='gitlab.com' 1318 | declare -r HOTFIX='hotfix' 1319 | declare -r DATE_FORMAT='+%Y-%m-%d' 1320 | declare -r DEFAULT_HOTFIX="${HOTFIX}-$(whoami)" 1321 | declare -r FEATURE='feature' 1322 | declare -r DEFAULT_FEATURE="${FEATURE}-$(whoami)" 1323 | declare -r UNRELEASED='Unreleased' 1324 | declare -r KEYWORDS=(Added Changed Deprecated Removed Fixed Security) 1325 | declare -r REFSHEADS='refs/heads' 1326 | declare -r REFSTAGS='refs/tags' 1327 | declare -r ARRP=(production prod live main master) 1328 | declare -r ARRS=(staging release rc preprod pre-production release-candidate prerelease) 1329 | declare -r ARRD=(dev devel develop next) 1330 | 1331 | # global variables 1332 | [[ -t 0 ]] 1333 | declare ISSTDIN=${?} 1334 | declare WHATNOW=0 1335 | declare DRYRUN=0 1336 | declare ENTRY=0 1337 | declare VERBOSE=0 1338 | declare YES=0 1339 | declare COLOR=auto 1340 | declare POSX=1 1341 | declare POSY=1 1342 | declare FORCE=0 1343 | declare STASHED=0 1344 | declare CONFORM=0 1345 | declare INIT=0 1346 | declare INITED=0 1347 | declare REQUEST=0 1348 | declare PULL=0 1349 | declare PUSH=0 1350 | declare MSGOPEN=0 1351 | 1352 | # process options 1353 | line=$(IFS=' ' getopt -n "${0}" \ 1354 | -o cefhinrvVwy\? \ 1355 | -l conform,color::,colour::,auto-entry,force,help,init,dry-run,request,verbose,version,what-now,yes,pull,push \ 1356 | -- "${@}" 2>&1) \ 1357 | || err "${line}" 2 1358 | eval set -- "${line}" 1359 | 1360 | while [[ ${#} -gt 0 ]]; do 1361 | case ${1} in 1362 | -c|--conform) CONFORM=1; shift ;; 1363 | -e|--auto-entry) ENTRY=1; shift ;; 1364 | --color|--colour) shift; setcolor "${1}"; shift ;; 1365 | -f|--force) FORCE=1; shift ;; 1366 | -h|-\?|--help) flow_usage; exit ;; 1367 | -i|--init) INIT=1; CONFORM=1; shift ;; 1368 | -n|--dry-run) DRYRUN=1; shift ;; 1369 | --pull) PULL=1; shift ;; 1370 | --push) PUSH=1; shift ;; 1371 | -r|--request) REQUEST=1; shift ;; 1372 | -v|--verbose) VERBOSE=1; shift ;; 1373 | -V|--version) flow_version; exit ;; 1374 | -w|--what-now) WHATNOW=1; shift ;; 1375 | -y|--yes) YES=1; shift ;; 1376 | --) shift; break ;; 1377 | *-) err "Unrecognized option '${1}'." 2 ;; 1378 | *) break ;; 1379 | esac 1380 | done 1381 | 1382 | # flow process 1383 | [[ ${#} -gt 1 ]] \ 1384 | && err 'Too many arguments.' 2 1385 | validate_requirements 1386 | validate_repository 1387 | validate_status_empty 1388 | [[ ${PULL} == 1 ]] \ 1389 | && flow_pull \ 1390 | && flow_finalize 1391 | flow_validate "${@}" 1392 | [[ ${INIT} == 1 ]] \ 1393 | && flow_finalize 1394 | [[ ${PUSH} == 1 ]] \ 1395 | && flow_push \ 1396 | && flow_finalize 1397 | [[ ${WHATNOW} == 1 ]] \ 1398 | && flow_whatnow \ 1399 | && flow_finalize 1400 | flow_action "${@}" \ 1401 | && flow_finalize 1402 | 1403 | } 1404 | 1405 | main "${@}" 1406 | --------------------------------------------------------------------------------