├── .github ├── dependabot.yml └── workflows │ └── attach_files_to_release.yml ├── LICENSE.md ├── README.md ├── conventional-commits_v1.0.0.md └── create-conventional-changelog /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/attach_files_to_release.yml: -------------------------------------------------------------------------------- 1 | name: Attach Files To Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - name: Set vars 15 | id: vars 16 | run: echo ::set-output name=tag::${GITHUB_REF/refs\/tags\//} 17 | - name: Clone repository 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ env.GITHUB_REF }} 21 | - name: sha256sums 22 | run: | 23 | sha256sum create-conventional-changelog > sha256sums.txt 24 | - name: Attach files to release 25 | env: 26 | RELEASE_TAG: ${{ steps.vars.outputs.tag }} 27 | run: >- 28 | hub release edit 29 | -m "" 30 | -a ./create-conventional-changelog 31 | -a ./sha256sums.txt 32 | $RELEASE_TAG 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-conventional-changelog 2 | 3 | A script for automatically generating a changelog from git history which uses conventional commit messages. 4 | ## 5 | 6 | 7 | 8 | ### Contents 9 | - [Downloads](#Downloads) 10 | - [Installation](#Installation) 11 | - [Usage](#Usage) 12 | ## 13 | 14 | 15 | 16 | ### Downloads 17 | 18 | Latest version is `v0.2.0`. 19 | 20 | - [GitHub releases](https://github.com/termux/create-conventional-changelog/releases). 21 | ## 22 | 23 | 24 | 25 | ### Installation 26 | 27 | 1. Run `apt install curl` to install `curl` first and download `create-conventional-changelog`. 28 | 29 | - Latest release: 30 | 31 | `curl -L 'https://github.com/termux/create-conventional-changelog/releases/latest/download/create-conventional-changelog' -o create-conventional-changelog` 32 | 33 | - Specific release: 34 | 35 | `curl -L 'https://github.com/termux/create-conventional-changelog/releases/download/v0.1.0/create-conventional-changelog' -o create-conventional-changelog` 36 | 37 | - Master Branch *may be unstable*: 38 | 39 | `curl -L 'https://github.com/termux/create-conventional-changelog/raw/master/create-conventional-changelog' -o create-conventional-changelog` 40 | 41 | 2. Give executable permissions. 42 | 43 | `chmod +x create-conventional-changelog` 44 | ## 45 | 46 | 47 | 48 | ### Usage 49 | 50 | - **`create-conventional-changelog`** 51 | 52 | As per conventional commits 1.0.0 specs, the commit message should be structured as follows: 53 | 54 | ``` 55 | [optional scope]: 56 | 57 | [optional body] 58 | 59 | [optional footer(s)] 60 | ``` 61 | 62 | Check [` conventional-commits_v1.0.0.md`](conventional-commits_v1.0.0.md) or https://www.conventionalcommits.org/en/v1.0.0 for details on the spec. 63 | 64 | The script will add commit messages (subject+body) and their hashes under a markdown heading for the type defined in the commit messages. Further post processing will have to be manually done. All commits that don't match the format with be added under the `` type. The script considers the types as case-insensitive and are converted to title case for headings. 65 | 66 | Make sure to replace `LAST_RELEASE_TAG` at end of changelog file in the compare url. 67 | 68 | ##### Help: 69 | 70 | ``` 71 | create-conventional-changelog is a script for automatically generating a 72 | changelog from git history which uses conventional commit messages. 73 | https://www.conventionalcommits.org/en/v1.0.0 74 | 75 | 76 | Usage: 77 | create-conventional-changelog [command_options] git_dir_path changelog_file_path start_commit_hash end_commit_hash repo_url release_tag 78 | 79 | Available command_options: 80 | [ -h | --help ] display this help screen 81 | [ --version ] display version 82 | [ -v | -vv ] set verbose level to 1 or 2 83 | 84 | 85 | start_commit_hash should be latest commit hash of previous release. 86 | 87 | end_commit_hash should be latest commit hash of current release. 88 | 89 | repo_url must be in the format: "https://host//. 90 | Example: "https://github.com/termux/termux-app" 91 | 92 | release_tag must be a valid git tag. 93 | Check http://git-scm.com/docs/git-check-ref-format for details. 94 | 95 | Examples: 96 | create-conventional-changelog termux-app changelog.md 9272a757 6c24e6ac https://github.com/termux/termux-app v0.118.0 97 | ``` 98 | ## 99 | -------------------------------------------------------------------------------- /conventional-commits_v1.0.0.md: -------------------------------------------------------------------------------- 1 | # Conventional Commits 1.0.0 2 | 3 | ## Summary 4 | 5 | The Conventional Commits specification is a lightweight convention on top of commit messages. 6 | It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. 7 | This convention dovetails with [SemVer](http://semver.org), by describing the features, fixes, and breaking changes made in commit messages. 8 | 9 | The commit message should be structured as follows: 10 | 11 | --- 12 | 13 | ``` 14 | [optional scope]: 15 | 16 | [optional body] 17 | 18 | [optional footer(s)] 19 | ``` 20 | --- 21 | 22 |
23 | The commit contains the following structural elements, to communicate intent to the consumers of your library: 24 | 25 | - **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning). 26 | - **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning). 27 | - **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning). 28 | A BREAKING CHANGE can be part of commits of any _type_. 29 | - _types_ other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [the Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines)) recommends `build:`, `chore:`, 30 | `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others. 31 | - _footers_ other than `BREAKING CHANGE: ` may be provided and follow a convention similar to 32 | [git trailer format](https://git-scm.com/docs/git-interpret-trailers). 33 | 34 | Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). 35 |

36 | A scope may be provided to a commit's type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`. 37 | 38 | ## Examples 39 | 40 | ### Commit message with description and breaking change footer 41 | ``` 42 | feat: allow provided config object to extend other configs 43 | 44 | BREAKING CHANGE: `extends` key in config file is now used for extending other config files 45 | ``` 46 | 47 | ### Commit message with `!` to draw attention to breaking change 48 | ``` 49 | feat!: send an email to the customer when a product is shipped 50 | ``` 51 | 52 | ### Commit message with scope and `!` to draw attention to breaking change 53 | ``` 54 | feat(api)!: send an email to the customer when a product is shipped 55 | ``` 56 | 57 | ### Commit message with both `!` and BREAKING CHANGE footer 58 | ``` 59 | chore!: drop support for Node 6 60 | 61 | BREAKING CHANGE: use JavaScript features not available in Node 6. 62 | ``` 63 | 64 | ### Commit message with no body 65 | ``` 66 | docs: correct spelling of CHANGELOG 67 | ``` 68 | 69 | ### Commit message with scope 70 | ``` 71 | feat(lang): add polish language 72 | ``` 73 | 74 | ### Commit message with multi-paragraph body and multiple footers 75 | ``` 76 | fix: prevent racing of requests 77 | 78 | Introduce a request id and a reference to latest request. Dismiss 79 | incoming responses other than from latest request. 80 | 81 | Remove timeouts which were used to mitigate the racing issue but are 82 | obsolete now. 83 | 84 | Reviewed-by: Z 85 | Refs: #123 86 | ``` 87 | 88 | ## Specification 89 | 90 | The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 91 | 92 | - Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space. 93 | - The type `feat` MUST be used when a commit adds a new feature to your application or library. 94 | - The type `fix` MUST be used when a commit represents a bug fix for your application. 95 | - A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., `fix(parser):` 96 | - A description MUST immediately follow the colon and space after the type/scope prefix. 97 | The description is a short summary of the code changes, e.g., _fix: array parsing issue when multiple spaces were contained in string_. 98 | - A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. 99 | - A commit body is free-form and MAY consist of any number of newline separated paragraphs. 100 | - One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a `:` or `#` separator, followed by a string value (this is inspired by the [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)). 101 | - A footer's token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate the footer section from a multi-paragraph body). An exception is made for `BREAKING CHANGE`, which MAY also be used as a token. 102 | - A footer's value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer token/separator pair is observed. 103 | - Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer. 104 | - If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., BREAKING CHANGE: environment variables now take precedence over config files_. 105 | - If included in the type/scope prefix, breaking changes MUST be indicated by a `!` immediately before the `:`. If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change. 106 | - Types other than `feat` and `fix` MAY be used in your commit messages, e.g., _docs: updated ref docs._ 107 | - The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. 108 | - BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer. 109 | 110 | ## Why Use Conventional Commits 111 | 112 | - Automatically generating CHANGELOGs. 113 | - Automatically determining a semantic version bump (based on the types of commits landed). 114 | - Communicating the nature of changes to teammates, the public, and other stakeholders. 115 | - Triggering build and publish processes. 116 | - Making it easier for people to contribute to your projects, by allowing them to explore 117 | a more structured commit history. 118 | 119 | ## FAQ 120 | 121 | ### How should I deal with commit messages in the initial development phase? 122 | 123 | We recommend that you proceed as if you've already released the product. Typically *somebody*, even if it's your fellow software developers, is using your software. They'll want to know what's fixed, what breaks etc. 124 | 125 | ### Are the types in the commit title uppercase or lowercase? 126 | 127 | Any casing may be used, but it's best to be consistent. 128 | 129 | ### What do I do if the commit conforms to more than one of the commit types? 130 | 131 | Go back and make multiple commits whenever possible. Part of the benefit of Conventional Commits is its ability to drive us to make more organized commits and PRs. 132 | 133 | ### Doesn’t this discourage rapid development and fast iteration? 134 | 135 | It discourages moving fast in a disorganized way. It helps you be able to move fast long term across multiple projects with varied contributors. 136 | 137 | ### Might Conventional Commits lead developers to limit the type of commits they make because they'll be thinking in the types provided? 138 | 139 | Conventional Commits encourages us to make more of certain types of commits such as fixes. Other than that, the flexibility of Conventional Commits allows your team to come up with their own types and change those types over time. 140 | 141 | ### How does this relate to SemVer? 142 | 143 | `fix` type commits should be translated to `PATCH` releases. `feat` type commits should be translated to `MINOR` releases. Commits with `BREAKING CHANGE` in the commits, regardless of type, should be translated to `MAJOR` releases. 144 | 145 | ### How should I version my extensions to the Conventional Commits Specification, e.g. `@jameswomack/conventional-commit-spec`? 146 | 147 | We recommend using SemVer to release your own extensions to this specification (and encourage you to make these extensions!) 148 | 149 | ### What do I do if I accidentally use the wrong commit type? 150 | 151 | #### When you used a type that's of the spec but not the correct type, e.g. `fix` instead of `feat` 152 | 153 | Prior to merging or releasing the mistake, we recommend using `git rebase -i` to edit the commit history. After release, the cleanup will be different according to what tools and processes you use. 154 | 155 | #### When you used a type *not* of the spec, e.g. `feet` instead of `feat` 156 | 157 | In a worst case scenario, it's not the end of the world if a commit lands that does not meet the Conventional Commits specification. It simply means that commit will be missed by tools that are based on the spec. 158 | 159 | ### Do all my contributors need to use the Conventional Commits specification? 160 | 161 | No! If you use a squash based workflow on Git lead maintainers can clean up the commit messages as they're merged—adding no workload to casual committers. 162 | A common workflow for this is to have your git system automatically squash commits from a pull request and present a form for the lead maintainer to enter the proper git commit message for the merge. 163 | 164 | ### How does Conventional Commits handle revert commits? 165 | 166 | Reverting code can be complicated: are you reverting multiple commits? if you revert a feature, should the next release instead be a patch? 167 | 168 | Conventional Commits does not make an explicit effort to define revert behavior. Instead we leave it to tooling authors to use the flexibility of _types_ and _footers_ to develop their logic for handling reverts. 169 | 170 | One recommendation is to use the `revert` type, and a footer that references the commit SHAs that are being reverted: 171 | 172 | ``` 173 | revert: let us never again speak of the noodle incident 174 | 175 | Refs: 676104e, a215868 176 | ``` 177 | 178 | Check latest version at https://github.com/conventional-commits/conventionalcommits.org/blob/master/content/v1.0.0/index.md -------------------------------------------------------------------------------- /create-conventional-changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2039,SC2059 3 | 4 | # Title: create-conventional-changelog 5 | # Description: A script for automatically generating a changelog from 6 | # git history which uses conventional commit messages. 7 | # https://www.conventionalcommits.org/en/v1.0.0 8 | # Author: agnostic-apollo 9 | # Usage: run "create-conventional-changelog" 10 | # Date: 06-Jan-2022 11 | # Bash Version: 4.3 or higher 12 | # Credits: - 13 | version=0.2.0 14 | 15 | 16 | 17 | ### Set Default Variables Start 18 | # The following variables must not be modified unless you know what you are doing 19 | 20 | CCC_VERBOSE_LEVEL=0 # Default to 0 21 | CCC_ARGS_VERBOSE_LEVEL=0 # Set this to 1 manually, if you want to debug arguments received 22 | 23 | CCC_NOOP_COMMAND=0 # Default to 0 24 | 25 | GIT_DIR_PATH="" # Default to none 26 | CHANGELOG_FILE_PATH="" # Default to none 27 | START_COMMIT_HASH="" # Default to none 28 | END_COMMIT_HASH="" # Default to none 29 | REPO_URL="" # Default to none 30 | RELEASE_TAG="" # Default to none 31 | 32 | ### Set Default Variables End 33 | 34 | function ccc_log() { local log_level="${1}"; shift; if [[ $CCC_VERBOSE_LEVEL -ge $log_level ]]; then echo "$@"; fi } 35 | function ccc_log_literal() { local log_level="${1}"; shift; if [[ $CCC_VERBOSE_LEVEL -ge $log_level ]]; then echo -e "$@"; fi } 36 | function ccc_log_errors() { echo "$@" 1>&2; } 37 | function ccc_log_args() { if [[ $CCC_ARGS_VERBOSE_LEVEL -ge "1" ]]; then echo "$@"; fi } 38 | function ccc_log_arg_errors() { echo "$@" 1>&2; } 39 | 40 | ## 41 | # ccc_main `[argument...]` 42 | ## 43 | ccc_main() { 44 | 45 | local return_value 46 | 47 | # Process the command arguments passed to create-conventional-changelog 48 | ccc_process_arguments "$@" || return $? 49 | 50 | [ "$CCC_NOOP_COMMAND" = "1" ] && return 0 51 | 52 | ccc_run 53 | 54 | } 55 | 56 | ## 57 | # ccc_run 58 | ## 59 | ccc_run() { 60 | 61 | local return_value 62 | 63 | local commit_hashes_list 64 | local commit_hash 65 | local commit_hash_short 66 | local commit_message 67 | local commit_subject 68 | local commit_pull_request_owner 69 | local commit_pull_request_info 70 | local commit_pull_request_number 71 | local commit_type 72 | local commit_type_and_scope 73 | local commit_type_title 74 | local commit_url_achor 75 | local commit_url_achor 76 | local is_breaking_change 77 | local changelog_text 78 | local release_timestamp 79 | local repo_owner 80 | local repo_url_escaped 81 | 82 | ccc_log 1 "GIT_DIR_PATH=\"$GIT_DIR_PATH\"" 83 | ccc_log 1 "CHANGELOG_FILE_PATH=\"$CHANGELOG_FILE_PATH\"" 84 | ccc_log 1 "START_COMMIT_HASH=\"$START_COMMIT_HASH\"" 85 | ccc_log 1 "END_COMMIT_HASH=\"$END_COMMIT_HASH\"" 86 | ccc_log 1 "REPO_URL=\"$REPO_URL\"" 87 | ccc_log 1 "RELEASE_TAG=\"$RELEASE_TAG\"" 88 | 89 | # If GIT_DIR_PATH is not set 90 | if [ -z "$GIT_DIR_PATH" ]; then 91 | ccc_log_arg_errors "git_dir_path passed is not set" 92 | return 1 93 | fi 94 | 95 | # If directory not found at GIT_DIR_PATH 96 | if [ ! -d "$GIT_DIR_PATH" ]; then 97 | ccc_log_arg_errors "git_dir_path \"$GIT_DIR_PATH\" not found" 98 | return 1 99 | fi 100 | 101 | # If ".git" directory not found under GIT_DIR_PATH 102 | if [ ! -d "$GIT_DIR_PATH/.git" ]; then 103 | ccc_log_arg_errors "git_dir_path \"$GIT_DIR_PATH\" does not contain the \".git\" directory" 104 | return 1 105 | fi 106 | 107 | # If CHANGELOG_FILE_PATH is not set 108 | if [ -z "$CHANGELOG_FILE_PATH" ]; then 109 | ccc_log_arg_errors "changelog_file_path passed is not set" 110 | return 1 111 | fi 112 | 113 | # If START_COMMIT_HASH is not set 114 | if [ -z "$START_COMMIT_HASH" ]; then 115 | ccc_log_arg_errors "start_commit_hash passed is not set" 116 | return 1 117 | fi 118 | 119 | # If END_COMMIT_HASH is not set 120 | if [ -z "$END_COMMIT_HASH" ]; then 121 | ccc_log_arg_errors "end_commit_hash passed is not set" 122 | return 1 123 | fi 124 | 125 | # If REPO_URL is not set 126 | if [ -z "$REPO_URL" ]; then 127 | ccc_log_arg_errors "repo_url passed is not set" 128 | return 1 129 | fi 130 | 131 | local valid_repo_url_regex='^https:\/\/([^/]+)\/([^/]+)\/([^/]+)$' 132 | if ccc_contains_newline "$REPO_URL" || [[ ! "$REPO_URL" =~ $valid_repo_url_regex ]]; then 133 | ccc_log_arg_errors "repo_url \"$REPO_URL\" passed is not a valid https url in the format https://host//. Example: https://github.com/termux/termux-app" 134 | return 1 135 | fi 136 | 137 | repo_url_escaped="$(ccc_get_escaped_string_for_sed "$REPO_URL")" 138 | repo_owner="$(printf "%s" "$REPO_URL" | sed -r -z -e "s/$valid_repo_url_regex/\2/")" 139 | 140 | # If RELEASE_TAG is not set 141 | if [ -z "$RELEASE_TAG" ]; then 142 | ccc_log_arg_errors "release_tag passed is not set" 143 | return 1 144 | fi 145 | 146 | if ! git check-ref-format "tags/$RELEASE_TAG" 1>/dev/null; then 147 | ccc_log_arg_errors "release_tag \"$RELEASE_TAG\" passed is not a valid git tag. Check http://git-scm.com/docs/git-check-ref-format for details." 148 | return 1 149 | fi 150 | 151 | # Get commit_hashes_list between start and end commit 152 | commit_hashes_list="$(git -C "$GIT_DIR_PATH" rev-list --ancestry-path "$START_COMMIT_HASH".."$END_COMMIT_HASH" 2>&1)" 153 | return_value=$? 154 | if [ $return_value -ne 0 ] || [ -z "$commit_hashes_list" ]; then 155 | ccc_log_errors "Failed to get commit hashes list between start and end commit" 156 | ccc_log_errors "$commit_hashes_list" 157 | if [ $return_value -eq 0 ]; then 158 | return_value=1 159 | fi 160 | return $return_value 161 | fi 162 | 163 | ccc_log 2 "commit_hashes_list=" 164 | ccc_log 2 "\`\`\`" 165 | ccc_log 2 "$commit_hashes_list" 166 | ccc_log 2 "\`\`\`" 167 | 168 | declare -A commit_types_list 169 | 170 | # As per conventional commits 1.0.0 specs, the commit message should 171 | # be structured as follows: 172 | # ``` 173 | # [optional scope]: 174 | # 175 | # [optional body] 176 | # 177 | # [optional footer(s)] 178 | # ``` 179 | # https://www.conventionalcommits.org/en/v1.0.0 180 | 181 | # Commit type can contain "a-zA-Z0-9_-" but must not start or end with "_-" 182 | # and must contain at least two characters 183 | local commit_types_regex='([a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]([|/][a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9])*)' # "\1" 184 | local commit_scope_regex='(\([^)]*\))?' # "\3" 185 | local commit_breaking_change_regex='(!)?' # "\4" 186 | local commit_subject_regex='(.*)' # "\5" 187 | local valid_commit_type_and_scope_regex="^$commit_types_regex$commit_scope_regex$commit_breaking_change_regex: $commit_subject_regex\$" 188 | ccc_log 2 "valid_commit_type_and_scope_regex=\"$valid_commit_type_and_scope_regex\"" 189 | 190 | local valid_merge_pull_request_commit_message_regex='^Merge pull request #([0-9]+) from ([^ \r\n/]+)\/[^ \r\n]+\r?\n\r?\n(.*)$' 191 | local commit_type_others='' 192 | 193 | local i=0 194 | for commit_hash in $commit_hashes_list; do 195 | commit_hash_short=${commit_hash:0:8} 196 | ccc_log_literal 1 "\n\nProcessing commit $i $commit_hash_short" 197 | 198 | # Get commit_message for the commit_hash 199 | commit_message="$(git -C "$GIT_DIR_PATH" log -n 1 --pretty="format:%B" "$commit_hash" 2>&1)" 200 | return_value=$? 201 | if [ $return_value -ne 0 ]; then 202 | ccc_log_errors "Failed to get commit message for the \"$commit_hash\" commit" 203 | ccc_log_errors "$commit_message" 204 | return $return_value 205 | fi 206 | 207 | # Remove leading whitespaces 208 | commit_message="$(printf "%s" "$commit_message" | sed -r -z -e 's/^[ \t\n]+//')" 209 | 210 | # Remove trailing whitespaces 211 | commit_message="$(printf "%s" "$commit_message" | sed -r -z -e 's/[ \t\n]+$//')" 212 | 213 | ccc_log 2 "commit_message=" 214 | ccc_log 2 "\`\`\`" 215 | ccc_log 2 "$commit_message" 216 | ccc_log 2 "\`\`\`" 217 | 218 | # If commit_message starts with "Merge pull request # from /", 219 | # then remove the line and add pull request info line. 220 | # Do not use `grep` since `grep -z` does not work with 221 | # start `^` and end `$` anchors 222 | # We manually add everything to pattern space and print only 223 | # if it matches the regex 224 | if [ -n "$(printf "%s\n" "$commit_message" | sed -r -n -e '$!{:a;N;$!ba;}; /'"$valid_merge_pull_request_commit_message_regex"'/p')" ]; then 225 | commit_pull_request_number="$(printf "%s" "$commit_message" | sed -r -n -e '$!{:a;N;$!ba;}; s/'"$valid_merge_pull_request_commit_message_regex"'/\1/;p')" 226 | commit_pull_request_owner="$(printf "%s" "$commit_message" | sed -r -n -e '$!{:a;N;$!ba;}; s/'"$valid_merge_pull_request_commit_message_regex"'/\2/;p')" 227 | commit_message="$(printf "%s" "$commit_message" | sed -r -n -e '$!{:a;N;$!ba;}; s/'"$valid_merge_pull_request_commit_message_regex"'/\3/;p')" 228 | 229 | # Remove leading whitespaces 230 | commit_message="$(printf "%s" "$commit_message" | sed -r -z -e 's/^[ \t\n]+//')" 231 | 232 | # If pull request was opened from branch of repo itself instead of a fork. 233 | if [[ "$commit_pull_request_owner" == "$repo_owner" ]]; then 234 | commit_pull_request_info="Implemented in [#$commit_pull_request_number]($REPO_URL/pull/$commit_pull_request_number)." 235 | else 236 | commit_pull_request_info="Implemented by @$commit_pull_request_owner in [#$commit_pull_request_number]($REPO_URL/pull/$commit_pull_request_number)." 237 | fi 238 | if ccc_contains_newline "$commit_message"; then 239 | commit_message+=$'\n\n'"$commit_pull_request_info" 240 | else 241 | if [[ "$commit_message" != *"." ]]; then 242 | commit_message+="." 243 | fi 244 | commit_message+=" $commit_pull_request_info" 245 | fi 246 | fi 247 | 248 | commit_subject="$(printf "%s" "$commit_message" | head -1)" 249 | 250 | # If commit_subject starts with a valid commit type and scope '[optional scope]: ' 251 | # Example: "Added: ", "Changed!: ", "Added|Changed: ", "Added/Changed: ", "Added(App): ", "Added(App: Settings): " 252 | is_breaking_change=0 253 | if printf "%s\n" "$commit_subject" | grep -zqE "$valid_commit_type_and_scope_regex"; then 254 | commit_type_and_scope="$(printf "%s" "$commit_subject" | sed -r -z -e "s/$valid_commit_type_and_scope_regex/\1\3\4/")" 255 | 256 | # Set commit_type to anything before first "|/" in case multiple types are defined 257 | commit_type="$(printf "%s" "$commit_subject" | sed -r -z -e "s/$valid_commit_type_and_scope_regex/\1/"| sed -r -z -e 's/^([^|/]+).*/\1/')" 258 | 259 | commit_message="$(printf "%s" "$commit_message" | sed -r -z -e "s/$valid_commit_type_and_scope_regex/\5/")" 260 | 261 | if [[ "$commit_type_and_scope" == *"!" ]]; then 262 | is_breaking_change=1 263 | fi 264 | else 265 | commit_type="$commit_type_others" 266 | fi 267 | 268 | if [ -z "$commit_type" ]; then 269 | ccc_log_arg_errors "Failed to fine commit_type for commit $i $commit_hash_short" 270 | return 1 271 | fi 272 | 273 | # Covert to title case 274 | commit_type_title="$(printf "%s" "$commit_type" | sed 's/[_-]/ /g' | sed 's/.*/\L&/; s/[a-z]*/\u&/g')" 275 | 276 | if [[ "$commit_message" == *"BREAKING CHANGE: "* ]]; then 277 | is_breaking_change=1 278 | fi 279 | 280 | ccc_log_literal 1 "subject: \"$commit_subject\", type: \"$commit_type_title\", is_breaking_change: \"$is_breaking_change\"" 281 | 282 | # Add commit url anchor at end of commit_message 283 | commit_url_achor="([\`$commit_hash_short\`]($REPO_URL/commit/$commit_hash_short))" 284 | if ccc_contains_newline "$commit_message"; then 285 | commit_message+=$'\n\n'"$commit_url_achor" 286 | else 287 | if [[ "$commit_message" != *"." ]]; then 288 | commit_message+="." 289 | fi 290 | commit_message+=" $commit_url_achor" 291 | fi 292 | 293 | 294 | # Replaces issue references with link anchor 295 | # Example: "closes #666" with "closes [#666]($REPO_URL/issues/666) 296 | # https://github.com/gitbucket/gitbucket/wiki/How-to-Close-Reference-issues-and-pull-request 297 | commit_message="$(printf "%s" "$commit_message" | sed -r -e 's/(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved|issue) #([0-9]+)/\1 [#\2\]('"${REPO_URL//\//\\\/}"'\/issues\/\2)/gI')" 298 | 299 | # Replaces issue link with link anchor 300 | # Example: "issue #666" with "issue [#666]($REPO_URL/issues/666) 301 | commit_message="$(printf "%s" "$commit_message" | sed -r -e 's/(issue:?) '"$repo_url_escaped"'\/issues\/([0-9]+)/\1 [#\2\]('"${REPO_URL//\//\\\/}"'\/issues\/\2)/gI')" 302 | 303 | # Add 4 spaces to start of each line so that markdown list does not break 304 | commit_message="$(printf "%s" "$commit_message" | sed -r -e 's/^(.)/ \1/g')" 305 | 306 | # Remove leading whitespaces 307 | commit_message="$(printf "%s" "$commit_message" | sed -r -z -e 's/^[ \t\n]+//')" 308 | 309 | if ccc_contains_newline "$commit_message"; then 310 | # Add two spaces to end of each line to prevent markdown list text wrap 311 | commit_message="$(printf "%s" "$commit_message" | sed -r -e 's/(.)$/\1 /g')" 312 | fi 313 | 314 | # If commit_type ends with "!" or commit_message contains "BREAKING CHANGE: " 315 | # to signify a breaking change, example: "Changed!:", then prefix 316 | # commit_message with "**!** " to show a bold exclamation mark 317 | if [[ "$is_breaking_change" == "1" ]]; then 318 | commit_message="**!** $commit_message" 319 | fi 320 | 321 | # Convert commit_message to markdown list by prefixing with "-" and uppercasing first letter 322 | commit_message="- ${commit_message^}" 323 | 324 | # Add commit_message to the commit_type_title key in the commit_types_list associative array 325 | if [[ -v commit_types_list["$commit_type_title"] ]]; then 326 | commit_types_list["$commit_type_title"]="${commit_types_list["$commit_type_title"]}"$'\n\n'"$commit_message" 327 | else 328 | commit_types_list["$commit_type_title"]="$commit_message" 329 | fi 330 | 331 | i=$((i + 1)) 332 | done 333 | 334 | release_timestamp="$(date -u +"%Y-%m-%d %H.%M")" 335 | 336 | ccc_log_literal 1 "\n\nCreating \"$RELEASE_TAG - $release_timestamp\" changelog file at \"$CHANGELOG_FILE_PATH\"" 337 | ccc_log_literal 1 "types: $(printf '%s\0' "${!commit_types_list[@]}" | sort -z | tr '\0' ',' | sed -r -e 's/,$//')" 338 | 339 | changelog_text="# Changelog" 340 | changelog_text+=$'\n\n'"## [$RELEASE_TAG] - $release_timestamp" 341 | 342 | while IFS= read -rd '' -u3 commit_type_title; do 343 | if [[ "$commit_type_title" != "$commit_type_others" ]]; then 344 | changelog_text+=$'\n\n'"### $commit_type_title"$'\n\n'"${commit_types_list["$commit_type_title"]}"$'\n' 345 | fi 346 | done 3< <(printf '%s\0' "${!commit_types_list[@]}" | sort -zn) 347 | 348 | if [[ -v commit_types_list["$commit_type_others"] ]]; then 349 | changelog_text+=$'\n\n'"### $commit_type_others"$'\n\n'"${commit_types_list["$commit_type_others"]}"$'\n' 350 | fi 351 | 352 | changelog_text+=$'\n'"##" 353 | changelog_text+=$'\n\n'"[$RELEASE_TAG]: $REPO_URL/compare/...$RELEASE_TAG"$'\n' 354 | 355 | # Create CHANGELOG_FILE_PATH parent directory if it does not exist 356 | ccc_create_parent_path CHANGELOG_FILE_PATH "$CHANGELOG_FILE_PATH" || return $? 357 | 358 | # Create changelog_text file at CHANGELOG_FILE_PATH 359 | printf "%s" "$changelog_text" > "$CHANGELOG_FILE_PATH" || return $? 360 | 361 | ccc_log_literal 1 "Changelog created successfully" 362 | 363 | return 0 364 | 365 | } 366 | 367 | ## 368 | # ccc_create_parent_path `label` `path` 369 | ## 370 | ccc_create_parent_path() { 371 | 372 | local return_value 373 | 374 | # If parameter count is not 2 375 | if [ $# -ne 2 ]; then 376 | ccc_log_errors "Invalid parameter count to \"ccc_create_parent_path\"" 377 | return 1 378 | fi 379 | 380 | local label="$1" 381 | local path="$2" 382 | 383 | local path_parent 384 | 385 | # Find path_parent 386 | path_parent=$(dirname "$path") 387 | return_value=$? 388 | if [ $return_value -ne 0 ]; then 389 | ccc_log_errors "Failure while finding dirname for $label \"$path\"" 390 | return $return_value 391 | fi 392 | 393 | # If path_parent exists and is not a directory 394 | if [ -e "$path_parent" ] && [ ! -d "$path_parent" ]; then 395 | ccc_log_errors "A non-directory file exists at parent path \"$path_parent\" of $label \"$path\"" 396 | return 1 397 | fi 398 | 399 | # Create path_parent if it does not exist 400 | if [ ! -d "$path_parent" ]; then 401 | mkdir -p "$path_parent" 402 | return_value=$? 403 | if [ $return_value -ne 0 ]; then 404 | ccc_log_errors "Failed to create parent directory of $label \"$path\"" 405 | return $return_value 406 | fi 407 | fi 408 | 409 | return 0 410 | 411 | } 412 | 413 | ## 414 | # Escape `[]/$*.^` with backslashes for sed 415 | # . 416 | # . 417 | # ccc_contains_newline `variable_value` 418 | ## 419 | ccc_get_escaped_string_for_sed() { 420 | 421 | # Test with: printf "%s" "[]/$.*^" | sed -zE -e 's/[][/$*.^]/\\&/g' 422 | printf "%s" "$1" | sed -zE -e 's/[][/$*.^]/\\&/g' 423 | 424 | } 425 | 426 | ## 427 | # ccc_contains_newline `variable_value` 428 | ## 429 | ccc_contains_newline() { 430 | 431 | [[ "$1" == *$'\n'* ]] 432 | 433 | } 434 | 435 | ## 436 | # ccc_trim_trailing_newlines `variable_name` `variable_value` 437 | ## 438 | ccc_trim_trailing_newlines() { 439 | 440 | local extglob_was_unset=1 441 | shopt extglob >/dev/null && extglob_was_unset=0 # Check if 'extglob' is currently set 442 | (( extglob_was_unset )) && shopt -s extglob # Set 'extglob', if currently unset 443 | 444 | local valid_bash_variable_name_regex='^[a-zA-Z][a-zA-Z0-9_]*(\[[0-9]+\])?$' 445 | 446 | # If $1 is valid bash variable_name 447 | if [[ "$1" =~ $valid_bash_variable_name_regex ]]; then 448 | # Set variable_name stored in $1 to variable_value in $2 without trailing newline and carriage return characters 449 | printf -v "$1" "%s" "${2%%*([$'\r\n'])}" 450 | else 451 | ccc_log_errors "variable_name \"$1\" passed to \"ccc_trim_trailing_newlines\" is not a valid bash variable name" 452 | fi 453 | 454 | (( extglob_was_unset )) && shopt -u extglob # Unset 'extglob', if previously unset 455 | 456 | } 457 | 458 | ## 459 | # ccc_process_arguments `[argument...]` 460 | ## 461 | ccc_process_arguments() { 462 | 463 | local opt; local arg; local OPTARG; local OPTIND 464 | 465 | # Parse options to create-conventional-changelog command 466 | while getopts ":hv-:" opt; do 467 | case ${opt} in 468 | -) 469 | arg="${OPTARG#*=}" 470 | case "${OPTARG}" in 471 | help) 472 | ccc_log_args "Parsing option: '--${OPTARG%=*}'" 473 | ccc_show_help 474 | CCC_NOOP_COMMAND=1; return 0 475 | ;; 476 | help*) 477 | ccc_log_arg_errors "Invalid option or parameters not allowed for option: '--${OPTARG%=*}'" 478 | ccc_exit_on_error || return $? 479 | ;; 480 | version) 481 | ccc_log_args "Parsing option: '--${OPTARG%=*}'" 482 | echo "$version" 483 | CCC_NOOP_COMMAND=1; return 0 484 | ;; 485 | version*) 486 | ccc_log_arg_errors "Invalid option or parameters not allowed for option: '--${OPTARG%=*}'" 487 | ccc_exit_on_error || return $? 488 | ;; 489 | '' ) # "--" terminates argument processing to support non-options that start with dashes 490 | ccc_log_args "Parsing option: '--'" 491 | break 492 | ;; 493 | *) 494 | ccc_log_arg_errors "Unknown option '--${OPTARG%=*}'" 495 | ccc_exit_on_error || return $? 496 | ;; 497 | esac 498 | ;; 499 | h) 500 | ccc_log_args "Parsing option: '-${opt}'" 501 | ccc_show_help 502 | CCC_NOOP_COMMAND=1; return 0 503 | ;; 504 | v) 505 | ccc_log_args "Parsing option: '-${opt}'" 506 | if [ "$CCC_VERBOSE_LEVEL" -lt "2" ]; then 507 | CCC_VERBOSE_LEVEL=$((CCC_VERBOSE_LEVEL+1)); 508 | else 509 | ccc_log_arg_errors "Invalid Option, max verbose level is 2" 510 | ccc_exit_on_error || return $? 511 | fi 512 | ;; 513 | \?) 514 | ccc_log_arg_errors "Unknown option: '-${OPTARG}'" 515 | ccc_exit_on_error || return $? 516 | ;; 517 | esac 518 | done 519 | shift $((OPTIND - 1)) # Remove already processed arguments from arguments array 520 | 521 | # If no parameters are received 522 | if [ $# -eq 0 ]; then 523 | ccc_show_help 524 | CCC_NOOP_COMMAND=1; return 0 525 | fi 526 | 527 | # If 6 parameters are not received 528 | if [ $# -ne 6 ]; then 529 | ccc_log_arg_errors "Invalid argument count. The \"create-conventional-changelog\" command expects 6 arguments: git_dir_path changelog_file_path start_commit_hash end_commit_hash repo_url release_tag" 530 | return 1 531 | fi 532 | 533 | ccc_trim_trailing_newlines "GIT_DIR_PATH" "$1" 534 | ccc_trim_trailing_newlines "CHANGELOG_FILE_PATH" "$2" 535 | ccc_trim_trailing_newlines "START_COMMIT_HASH" "$3" 536 | ccc_trim_trailing_newlines "END_COMMIT_HASH" "$4" 537 | ccc_trim_trailing_newlines "REPO_URL" "$5" 538 | ccc_trim_trailing_newlines "RELEASE_TAG" "$6" 539 | 540 | } 541 | 542 | ## 543 | # ccc_show_help 544 | ## 545 | ccc_show_help() { 546 | 547 | echo " 548 | create-conventional-changelog is a script for automatically generating a 549 | changelog from git history which uses conventional commit messages. 550 | https://www.conventionalcommits.org/en/v1.0.0 551 | 552 | 553 | Usage: 554 | create-conventional-changelog [command_options] git_dir_path changelog_file_path start_commit_hash end_commit_hash repo_url release_tag 555 | 556 | Available command_options: 557 | [ -h | --help ] display this help screen 558 | [ --version ] display version 559 | [ -v | -vv ] set verbose level to 1 or 2 560 | 561 | 562 | start_commit_hash should be latest commit hash of previous release. 563 | 564 | end_commit_hash should be latest commit hash of current release. 565 | 566 | repo_url must be in the format: https://host// 567 | Example: https://github.com/termux/termux-app 568 | 569 | release_tag must be a valid git tag. 570 | Check http://git-scm.com/docs/git-check-ref-format for details. 571 | 572 | Examples: 573 | create-conventional-changelog termux-app changelog.md 9272a757 6c24e6ac https://github.com/termux/termux-app v0.118.0 574 | " 575 | 576 | } 577 | 578 | ## 579 | # ccc_exit_on_error 580 | ## 581 | ccc_exit_on_error() { 582 | 583 | ccc_show_help 584 | return 1 585 | 586 | } 587 | 588 | # Run ccc_main function 589 | [[ x"${BASH_SOURCE[0]}" == x"$0" ]] && { ccc_main "$@"; exit $?; } 590 | --------------------------------------------------------------------------------