├── 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 |  |
6 | | 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 |
--------------------------------------------------------------------------------