Repository: nico2sh/semtag
Branch: master
Commit: 1600023e1c3a
Files: 7
Total size: 80.4 KB
Directory structure:
gitextract_v776qutd/
├── .gitignore
├── LICENSE
├── README.md
├── release.sh
├── semtag
├── semtag_bkp
└── test_semtag.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
### Vim ###
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
# if I open this with Intellij
.idea
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Semtag
Semantic Tagging Script for Git
[Version: v0.2.0]
Notes: *This script is inspired by the [Nebula Release Plugin](https://github.com/nebula-plugins/nebula-release-plugin), and borrows a couple of lines from [Semver Bash Tool](https://github.com/fsaintjacques/semver-tool) (mostly the version comparison and the semantic version regex).*
[A quick history of this script](https://medium.com/@dr_notsokind/semantic-tagging-with-git-1254dbded22)
This is a script to help out version bumping on a project following the [Semantic Versioning](http://semver.org/) specification. It uses Git Tags to keep track the versions and the commit log between them, so no extra files are needed. It can be combined with release scripts, git hooks, etc, to have a consistent versioning.
### Why Bash? (and requirements)
Portability, mostly. You can use the script in any project that uses Git as a version control system. The only requirement is Git.
### Why not use the Nebula-release plugin?
Nebula Release is for releasing and publishing components and tries to automate the whole process from tagging to publishing. The goal of the `semtag` script is to only tag release versions, leaving the release process up to the developer.
Plus, the `semtag` script doesn't depend on the build system (so no need to use Gradle), so it can be used in any project.
## Usage
Copy the `semtag` script in your project's directory.
Semtag distinguishes between final versions and non-final versions. Possible non-final versions are `alpha`, `beta` and `rc` (release candidate).
Starts from version `0.0.0`, so the first time you initialize a version, it will tag it with the following bumped one (`1.0.0` if major, `0.1.0` if minor, `0.0.1` if patch)
Use the script as follows:
```
semtag <command> <options>
```
Info commands:
* `getfinal` Returns the current final version.
* `getlast` Returns the last tagged version, it can be the final version or a non-final version.
* `getcurrent` Returns the current version, it can be the tagged final version or a tagged non-final version. If there are unstaged or uncommitted changes, they will be included in the version, following this format: `<major>.<minor>.<patch>-dev.#+<branch>.<hash>`. Where `#` is the number of commits since the last final release, `branch` will be the current branch if we are not in the default branch (`master`, `main`, or other) and `hash` is the git hash of the current commit.
* `get` Returns both last tagged version and current final version.
Versioning commands:
* `final` Bumps the version top a final version
* `alpha` Bumps the version top an alpha version (appending `-alpha.#` to the version.
* `beta` Bumps the version top a beta version (appending `-beta.#` to the version.
* `candidate` Bumps the version top an release candidate version (appending `-rc.#` to the version.
Note: If there are no commits since the last final version, the version is not bumped.
All versioning commands tags the project with the new version using annotated tags (the tag message contains the list of commits included in the tag), and pushes the tag to the origin remote.
If you don't want to tag, but just display which would be the next bumped version, use the flag `-o` for showing the output only.
For specifying the scope you want to bump the version, use the `-s <scope>` option. Possible scopes are `major`, `minor` and `patch`. There is also `auto` which will choose between `minor` and `patch` depending on the percentage of lines changed. Usually it should be the developers decisions which scope to use, since the percentage of lines is not a great criteria, but this option is to help giving a more meaningful versioning when using in automatic scripts.
If you want to manually set a version, use the `-v <version>` option. Version must comply the semantic versioning specification (`v<major>.<minor>.<patch>`), and must be higher than the latest version. Works with any versioning command.
### Usage Examples
See the `release` script as an example. The script gets the next version to tag, uses that version to update the `README.md` file (this one!), and the script's. Then commits the changes, and finally tags the project with this latest version.
#### Gradle example
For setting up your project's version, in your `build.gradle` file, add the following:
```
version=getVersionTag()
def getVersionTag() {
def hashStdOut = new ByteArrayOutputStream()
exec {
commandLine "$rootProject.projectDir/semtag", "getcurrent"
standardOutput = hashStdOut
}
return hashStdOut.toString().trim()
}
```
This way, the project's version every time you make a build, will be aligned with the tagged version. On your CI script, you can tag the release version before deploying, or alternatively, before publishing to a central repository (such as Artifactory), you can create a Gradle task tagging the release version:
```
def tagFinalVersion() {
exec {
commandLine "$rootProject.projectDir/semtag", "final", "-s minor"
standardOutput = hashStdOut
}
doLast {
project.version=getVersionTag()
}
}
artifactoryPublish.dependsOn tagFinalVersion
```
Or create your own task for tagging and releasing. The goal of this script is to provide flexibility on how to manage and deal with the releases and deploys.
## How does it bump
Semtag tries to guess which is the following version by using the current final version as a reference for bumping. For example:
```
$ semtag get
Current final version: v1.0.0
Last tagged version: v1.0.0
$ semtag candidate -s minor
$ semtag get
Current final version: v1.0.0
Last tagged version: v1.1.0-rc.1
```
Above it used the `v1.0.0` version for bumping a minor release candidate. If we try to increase a patch:
```
$ semtag candidate -s patch
$ semtag get
Current final version: v1.0.0
Last tagged version: v1.1.0-rc.2
```
Again, it used the `v1.0.0` version as a reference to increase the patch version (so it should be bumped to `v1.0.1-rc.1`), but since the last tagged version is higher, it bumped the release candidate number instead. If we release a beta version:
```
$ semtag beta -s patch
$ semtag get
Current final version: v1.0.0
Last tagged version: v1.1.1-beta.1
```
Now the patch has been bumped, since a beta version is considered to be lower than a release candidate, so is the verison number that bumps up, using the provided scope (`patch` in this case).
### Forcing a tag
Semtag doesn't tag if there are no new commits since the last version, or if there are unstaged changes. To force to tag, use the `-f` flag, then it will bump no matter if there are unstaged changes or no new commits.
### Version prefix
By default, semtag prefixes new versions with `v`. Use the `-p` (plain) flag to create new versions with no `v` prefix.
### Tag prefix
For projects managing multiple components in a single repository, you can use custom tag prefixes to organize versions independently. Use the `-P <prefix>` flag to add a custom prefix to your tags.
```bash
# Default behavior
semtag alpha -s patch # Creates: v0.0.1-alpha.1
# With custom prefix
semtag alpha -s patch -P service # Creates: service-v0.0.1-alpha.1
# Combine with -p flag for plain versions (no 'v')
semtag alpha -s patch -P api -p # Creates: api-0.0.1-alpha.1
```
Each tag prefix maintains its own independent version sequence, allowing you to version multiple components separately:
```bash
# Version the backend service
semtag final -s minor -P backend # Creates: backend-v0.1.0
# Version the frontend independently
semtag final -s patch -P frontend # Creates: frontend-v0.0.1
# Default tags remain separate
semtag final -s major # Creates: v1.0.0
```
Query versions by prefix:
```bash
semtag getlast -P backend # Returns: backend-v0.1.0
semtag getfinal -P frontend # Returns: frontend-v0.0.1
semtag getlast # Returns: v1.0.0
```
License
=======
Copyright 2020 Nico Hormazábal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: release.sh
================================================
#!/usr/bin/env bash
# release.sh - Release automation script (Refactored)
# Improved version with better error handling, validation, and maintainability
set -euo pipefail # Fail fast on errors, undefined variables, and pipe failures
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SEMTAG_SCRIPT="${SCRIPT_DIR}/semtag"
readonly README_FILE="${SCRIPT_DIR}/README.md"
# Configuration with defaults
declare -A CONFIG=(
[scope]="${1:-auto}"
[dry_run]="false"
[verbose]="false"
)
# Utility functions
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
}
error() {
log "ERROR: $*"
exit 1
}
verbose() {
[[ "${CONFIG[verbose]}" == "true" ]] && log "$*"
}
# Validate dependencies
check_dependencies() {
if [[ ! -f "$SEMTAG_SCRIPT" ]]; then
error "semtag script not found at: $SEMTAG_SCRIPT"
fi
if [[ ! -x "$SEMTAG_SCRIPT" ]]; then
log "Making semtag executable..."
chmod +x "$SEMTAG_SCRIPT"
fi
if [[ ! -f "$README_FILE" ]]; then
error "README.md not found at: $README_FILE"
fi
# Check if we're in a git repository
if ! git rev-parse --git-dir >/dev/null 2>&1; then
error "Not in a git repository"
fi
# Check for uncommitted changes (unless dry run)
if [[ "${CONFIG[dry_run]}" == "false" ]]; then
if ! git diff --quiet HEAD 2>/dev/null; then
error "Repository has uncommitted changes. Commit or stash them first."
fi
fi
}
# Validate scope parameter
validate_scope() {
local scope="${CONFIG[scope]}"
case "$scope" in
auto|major|minor|patch)
verbose "Using scope: $scope"
;;
*)
error "Invalid scope '$scope'. Must be one of: auto, major, minor, patch"
;;
esac
}
# Get next version without tagging
get_next_version() {
local version
verbose "Getting next version with scope '${CONFIG[scope]}'..."
# Use the semtag script to get next version (dry run mode)
if ! version=$("$SEMTAG_SCRIPT" final -o -s "${CONFIG[scope]}" 2>/dev/null); then
error "Failed to get next version from semtag"
fi
if [[ -z "$version" ]]; then
error "semtag returned empty version"
fi
verbose "Next version will be: $version"
echo "$version"
}
# Update version in files
update_version_in_files() {
local version="$1"
log "Updating version to $version in source files..."
# Create backup function for safer file operations
backup_file() {
local file="$1"
if [[ -f "$file" ]]; then
cp "$file" "${file}.backup"
verbose "Created backup: ${file}.backup"
fi
}
# Restore backup function
restore_backup() {
local file="$1"
if [[ -f "${file}.backup" ]]; then
mv "${file}.backup" "$file"
log "Restored backup for $file"
fi
}
# Clean backup function
cleanup_backup() {
local file="$1"
if [[ -f "${file}.backup" ]]; then
rm "${file}.backup"
verbose "Cleaned up backup: ${file}.backup"
fi
}
# Trap to cleanup backups on error
trap 'restore_backup "$SEMTAG_SCRIPT"; restore_backup "$README_FILE"' ERR
# Update semtag script version
backup_file "$SEMTAG_SCRIPT"
if ! sed -i '' "s/^PROG_VERSION=\"[^\"]*\"/PROG_VERSION=\"$version\"/g" "$SEMTAG_SCRIPT"; then
error "Failed to update version in $SEMTAG_SCRIPT"
fi
verbose "Updated version in semtag script"
# Update README.md version
backup_file "$README_FILE"
if ! sed -i '' "s/^\[Version: [^]]*\]/[Version: $version]/g" "$README_FILE"; then
error "Failed to update version in $README_FILE"
fi
verbose "Updated version in README.md"
# Verify changes were made
if ! grep -q "PROG_VERSION=\"$version\"" "$SEMTAG_SCRIPT"; then
error "Version update verification failed for $SEMTAG_SCRIPT"
fi
if ! grep -q "\\[Version: $version\\]" "$README_FILE"; then
error "Version update verification failed for $README_FILE"
fi
# Clean up backups on success
cleanup_backup "$SEMTAG_SCRIPT"
cleanup_backup "$README_FILE"
# Disable trap
trap - ERR
log "Successfully updated version in all files"
}
# Commit and push changes
commit_and_push_changes() {
local version="$1"
log "Committing changes for version $version..."
# Stage the modified files
if ! git add "$SEMTAG_SCRIPT" "$README_FILE"; then
error "Failed to stage modified files"
fi
# Commit with descriptive message
local commit_message="Update version info to $version
- Updated PROG_VERSION in semtag script
- Updated version badge in README.md
Automated release preparation commit."
if ! git commit -m "$commit_message"; then
error "Failed to commit changes"
fi
verbose "Changes committed successfully"
# Push changes
log "Pushing changes to remote..."
if ! git push; then
error "Failed to push changes to remote"
fi
log "Changes pushed successfully"
}
# Create final tag
create_final_tag() {
local version="$1"
log "Creating final tag for version $version..."
# Use semtag to create the final tag
if ! "$SEMTAG_SCRIPT" final -f -v "$version"; then
error "Failed to create final tag with semtag"
fi
log "Successfully created and pushed tag: $version"
}
# Show release summary
show_summary() {
local version="$1"
log "Release Summary:"
log " Version: $version"
log " Scope: ${CONFIG[scope]}"
log " Files updated: semtag, README.md"
log " Git tag created and pushed: $version"
log ""
log "Release completed successfully!"
}
# Dry run mode
perform_dry_run() {
local version
version=$(get_next_version)
log "DRY RUN MODE - No changes will be made"
log "Would release version: $version"
log "Would update files: $SEMTAG_SCRIPT, $README_FILE"
log "Would commit and push changes"
log "Would create and push git tag: $version"
}
# Parse command line arguments
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
CONFIG[dry_run]="true"
shift
;;
--verbose|-v)
CONFIG[verbose]="true"
shift
;;
--help|-h)
show_help
exit 0
;;
-*)
error "Unknown option: $1"
;;
*)
# Assume it's a scope parameter
CONFIG[scope]="$1"
shift
;;
esac
done
}
# Show help
show_help() {
cat << EOF
Usage: $(basename "$0") [SCOPE] [OPTIONS]
Release automation script for semtag.
Arguments:
SCOPE Version scope to bump (auto|major|minor|patch) [default: auto]
Options:
--dry-run Show what would be done without making changes
--verbose, -v Enable verbose output
--help, -h Show this help message
Examples:
$(basename "$0") # Auto-detect scope and release
$(basename "$0") minor # Release with minor version bump
$(basename "$0") --dry-run # Preview release actions
$(basename "$0") patch --verbose # Release patch with verbose output
This script will:
1. Get the next version using semtag
2. Update version strings in semtag and README.md
3. Commit and push the changes
4. Create and push a git tag using semtag
EOF
}
# Main execution function
main() {
# Parse arguments (excluding the scope which is handled in CONFIG initialization)
shift 2>/dev/null || true # Remove scope from $@ if present
parse_arguments "$@"
log "Starting release process with scope: ${CONFIG[scope]}"
# Validate everything first
check_dependencies
validate_scope
# Handle dry run
if [[ "${CONFIG[dry_run]}" == "true" ]]; then
perform_dry_run
return 0
fi
# Execute release process
local version
version=$(get_next_version)
update_version_in_files "$version"
commit_and_push_changes "$version"
create_final_tag "$version"
show_summary "$version"
}
# Only run main if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
================================================
FILE: semtag
================================================
#!/usr/bin/env bash
# Enable strict error handling
set -euo pipefail
PROG=semtag
PROG_VERSION="v0.1.2"
# Error handling function
error_exit() {
local error_message="$1"
local exit_code="${2:-1}"
echo "ERROR: $error_message" >&2
exit "$exit_code"
}
# Check if we're in a git repository
check_git_repo() {
if ! git rev-parse --git-dir >/dev/null 2>&1; then
error_exit "Not in a git repository. Please run this script from within a git repository."
fi
}
SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"
IDENTIFIER_REGEX="^\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$"
NUMERIC_REGEX='^[0-9]+$'
# Length limit for the branch name in build metadata.
MAX_BRANCH_LENGTH=50
# Global variables
FIRST_VERSION="v0.0.0"
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
hasversiontag="false"
scope="patch"
displayonly="false"
forcetag="false"
prefix="v"
tag_prefix=""
forcedversion=
versionname=
identifier=
HELP="\
Usage:
$PROG
$PROG getlast
$PROG getfinal
$PROG (final|alpha|beta|candidate) [-s <scope> (major|minor|patch|auto) | -o]
$PROG --help
$PROG --version
Options:
-s The scope that must be increased, can be major, minor or patch.
The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD)
where X, Y and Z are positive integers, PRERELEASE is an optionnal
string composed of alphanumeric characters describing if the build is
a release candidate, alpha or beta version, with a number.
BUILD is also an optional string composed of alphanumeric
characters and hyphens.
Setting the scope as 'auto', the script will chose the scope between
'minor' and 'patch', depending on the amount of lines added (<10% will
choose patch).
-v Specifies manually the version to be tagged, must be a valid semantic version
in the format X.Y.Z where X, Y and Z are positive integers.
-o Output the version only, shows the bumped version, but doesn't tag.
-f Forces to tag, even if there are unstaged or uncommited changes.
-p Use a plain version, ie. do not prefix with 'v'.
-P Add a custom prefix to the tag (e.g., -P service generates service-v0.0.1).
Commands:
--help Print this help message.
--version Prints the program's version.
get Returns both current final version and last tagged version.
getlast Returns the latest tagged version.
getfinal Returns the latest tagged final version.
getcurrent Returns the current version, based on the latest one, if there are uncommited or
unstaged changes, they will be reflected in the version, adding the number of
pending commits, current branch and commit hash.
final Tags the current build as a final version, this only can be done on the default branch.
candidate Tags the current build as a release candidate, the tag will contain all
the commits from the last final version.
alpha Tags the current build as an alpha version, the tag will contain all
the commits from the last final version.
beta Tags the current build as a beta version, the tag will contain all
the commits from the last final version."
# Validate input arguments
validate_scope() {
local scope="$1"
case "$scope" in
major|minor|patch|auto) ;;
*) error_exit "Invalid scope '$scope'. Valid scopes are: major, minor, patch, auto" ;;
esac
}
validate_version() {
local version="$1"
if [[ ! "$version" =~ $SEMVER_REGEX ]]; then
error_exit "Invalid version format '$version'. Must follow semantic versioning (e.g., 1.2.3)"
fi
}
# Commands and options
ACTION="${1:-getlast}"
if [[ $# -gt 0 ]]; then
shift
fi
# We get the parameters
while getopts "v:s:ofpP:" opt; do
case $opt in
v)
forcedversion="$OPTARG"
validate_version "$forcedversion"
;;
s)
scope="$OPTARG"
validate_scope "$scope"
;;
o)
displayonly="true"
;;
f)
forcetag="true"
;;
p)
prefix=""
;;
P)
tag_prefix="$OPTARG-"
;;
\?)
error_exit "Invalid option: -$OPTARG"
;;
:)
error_exit "Option -$OPTARG requires an argument."
;;
esac
done
# Try to programmatically fetch the default branch. Go by the first remote HEAD found, otherwise default to `master`.
# $1 The variable to store the result
function get_default_branch {
local __result="$1"
local __default_branch=""
local __remotes
if ! __remotes=$(git remote 2>/dev/null); then
error_exit "Failed to get git remotes"
fi
if [[ -n "$__remotes" ]]; then
while IFS= read -r __remote; do
local __default_branch_ref
if __default_branch_ref=$(git symbolic-ref --quiet "refs/remotes/${__remote}/HEAD" 2>/dev/null); then
__default_branch="${__default_branch_ref#refs/remotes/${__remote}/}"
if [[ -n "$__default_branch" ]]; then
break
fi
fi
done <<< "$__remotes"
fi
printf -v "$__result" '%s' "${__default_branch:-master}"
}
# Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version
# $1 The string containing the version in semantic format
# $2 The variable to store the result array:
# position 0: major number
# position 1: minor number
# position 2: patch number
# position 3: identifier (or prerelease identifier)
# position 4: build info
function explode_version {
local __version="$1"
local __result="$2"
if [[ "$__version" =~ $SEMVER_REGEX ]] ; then
local __major="${BASH_REMATCH[1]}"
local __minor="${BASH_REMATCH[2]}"
local __patch="${BASH_REMATCH[3]}"
local __prere="${BASH_REMATCH[4]}"
local __build="${BASH_REMATCH[5]}"
# Use eval for compatibility with older bash versions
eval "$__result=(\"\$__major\" \"\$__minor\" \"\$__patch\" \"\$__prere\" \"\$__build\")"
else
# Clear the array
eval "$__result=()"
fi
}
# Compare two versions and returns -1, 0 or 1
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the result
function compare_versions {
local __first __second
explode_version "$1" __first
explode_version "$2" __second
local lv="$3"
# Compares MAJOR, MINOR and PATCH
for i in 0 1 2; do
local __numberfirst="${__first[$i]:-0}"
local __numbersecond="${__second[$i]:-0}"
local __diff=$(((__numberfirst) - (__numbersecond)))
case "$__diff" in
0)
;;
-*)
printf -v "$lv" '%s' "-1"
return 0
;;
*)
printf -v "$lv" '%s' "1"
return 0
;;
esac
done
# Identifiers should compare with the ASCII order.
local compareresult
compare_identifiers "${__first[3]:-}" "${__second[3]:-}" compareresult
printf -v "$lv" '%s' "$compareresult"
}
# Returns the number comparison
# $1 The first number to compare
# $2 The second number to compare
# $3 The variable where to store the result
function compare_numeric {
local __first="$1"
local __second="$2"
local __result="$3"
if (( __first < __second )) ; then
printf -v "$__result" '%s' "-1"
elif (( __first > __second )) ; then
printf -v "$__result" '%s' "1"
else
printf -v "$__result" '%s' "0"
fi
}
# Returns the alpanumeric comparison
# $1 The first alpanumeric to compare
# $2 The second alpanumeric to compare
# $3 The variable where to store the result
function compare_alphanumeric {
local __first="$1"
local __second="$2"
local __result="$3"
if [[ "$__first" < "$__second" ]] ; then
printf -v "$__result" '%s' "-1"
elif [[ "$__first" > "$__second" ]] ; then
printf -v "$__result" '%s' "1"
else
printf -v "$__result" '%s' "0"
fi
}
# Returns the last version of two
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the last one
function get_latest_of_two {
local __first="$1"
local __second="$2"
local __result
local __latest="$3"
compare_versions "$__first" "$__second" __result
case "$__result" in
0)
printf -v "$__latest" '%s' "$__second"
;;
-1)
printf -v "$__latest" '%s' "$__second"
;;
1)
printf -v "$__latest" '%s' "$__first"
;;
esac
}
# Returns comparison of two identifier parts
# $1 The first part to compare
# $2 The second part to compare
# $3 The variable where to store the compare result
function compare_identifier_part {
local __first=$1
local __second=$2
local __result=$3
local compareresult
if [[ "$__first" =~ $NUMERIC_REGEX ]] && [[ "$__second" =~ $NUMERIC_REGEX ]] ; then
compare_numeric "$__first" "$__second" compareresult
eval "$__result=$compareresult"
return 0
fi
compare_alphanumeric "$__first" "$__second" compareresult
eval "$__result=$compareresult"
}
# Returns comparison of two identifiers
# $1 The first identifier to compare
# $2 The second identifier to compare
# $3 The variable where to store the compare result
function compare_identifiers {
local __first=$1
local __second=$2
local __result=$3
local partresult
local arraylengths
if [[ -n "$__first" ]] && [[ -n "$__second" ]]; then
explode_identifier "${__first}" explodedidentifierfirst
explode_identifier "${__second}" explodedidentifiersecond
firstsize=${#explodedidentifierfirst[@]}
secondsize=${#explodedidentifiersecond[@]}
minlength=$(( $firstsize<$secondsize ? $firstsize : $secondsize ))
for (( i = 0 ; i < $minlength ; i++ )); do
compare_identifier_part "${explodedidentifierfirst[$i]}" "${explodedidentifiersecond[$i]}" partresult
case $partresult in
0)
;;
*)
eval "$__result=$partresult"
return 0
;;
esac
done
compare_numeric $firstsize $secondsize arraylengths
eval "$__result=$arraylengths"
return 0
elif [[ -z "$__first" ]] && [[ -n "$__second" ]]; then
eval "$__result=1"
return 0
elif [[ -n "$__first" ]] && [[ -z "$__second" ]]; then
eval "$__result=-1"
return 0
fi
eval "$__result=0"
}
# Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1
# $1 The identifier in the format -id.#
# $2 The vferiable where to store the 2 size array
function explode_identifier {
local __identifier="$1"
local __result="$2"
if [[ "$__identifier" =~ $IDENTIFIER_REGEX ]] ; then
local identifierparts
IFS='-.' read -ra identifierparts <<< "$__identifier"
# Filter out empty elements (the first element is empty when splitting "-beta.1")
local filtered=()
for part in "${identifierparts[@]}"; do
if [[ -n "$part" ]]; then
filtered+=("$part")
fi
done
eval "$__result=( \"\${filtered[@]}\" )"
else
eval "$__result=()"
fi
}
# Gets a list of tags and assigns the base and latest versions
# Receives an array with the tags containing the versions
# Assigns to the global variables finalversion and lastversion the final version and the latest version
function get_latest {
local __taglist=("$@")
local __tagsnumber=${#__taglist[@]}
local __current
case $__tagsnumber in
0)
finalversion="$FIRST_VERSION"
lastversion="$FIRST_VERSION"
;;
1)
__current="${__taglist[0]}"
local ver
explode_version "$__current" ver
if [ ${#ver[@]} -gt 0 ]; then
if [ -n "${ver[3]:-}" ]; then
finalversion="$FIRST_VERSION"
else
finalversion="$__current"
fi
lastversion="$__current"
else
finalversion="$FIRST_VERSION"
lastversion="$FIRST_VERSION"
fi
;;
*)
local __lastpos=$((__tagsnumber-1))
for i in $(seq 0 "$__lastpos")
do
__current="${__taglist[i]}"
local ver
explode_version "$__current" ver
if [ ${#ver[@]} -gt 0 ]; then
if [ -z "${ver[3]:-}" ]; then
get_latest_of_two "$finalversion" "$__current" finalversion
get_latest_of_two "$lastversion" "$finalversion" lastversion
else
get_latest_of_two "$lastversion" "$__current" lastversion
fi
fi
done
;;
esac
if git rev-parse -q --verify "refs/tags/$tag_prefix$lastversion" >/dev/null 2>&1; then
hasversiontag="true"
else
hasversiontag="false"
fi
}
# Gets the next version given the provided scope
# $1 The version that is going to be bumped
# $2 The scope to bump
# $3 The variable where to stoer the result
function get_next_version {
local __exploded
local __fromversion="$1"
local __scope="$2"
local __result="$3"
explode_version "$__fromversion" __exploded
case "$__scope" in
major)
__exploded[0]=$((${__exploded[0]}+1))
__exploded[1]=0
__exploded[2]=0
;;
minor)
__exploded[1]=$((${__exploded[1]}+1))
__exploded[2]=0
;;
patch)
__exploded[2]=$((${__exploded[2]}+1))
;;
esac
printf -v "$__result" '%s' "${prefix}${__exploded[0]}.${__exploded[1]}.${__exploded[2]}"
}
function bump_version {
## First we try to get the next version based on the existing last one
if [ "$scope" == "auto" ]; then
get_scope_auto scope
fi
local __candidatefromlast=$FIRST_VERSION
local __explodedlast
explode_version $lastversion __explodedlast
if [[ -n "${__explodedlast[3]}" ]]; then
# Last version is not final
local __idlast
explode_identifier ${__explodedlast[3]} __idlast
# We get the last, given the desired id based on the scope
__candidatefromlast="${prefix}${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}"
if [[ -n "$identifier" ]]; then
local __nextid="$identifier.1"
if [ "$identifier" == "${__idlast[0]}" ]; then
# We target the same identifier as the last so we increase one
__nextid="$identifier.$(( ${__idlast[1]}+1 ))"
__candidatefromlast="$__candidatefromlast-$__nextid"
else
# Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version
__candidatefromlast="$__candidatefromlast-$__nextid"
local __comparedwithlast
compare_versions $__candidatefromlast $lastversion __comparedwithlast
if [ "$__comparedwithlast" == -1 ]; then
get_next_version $__candidatefromlast $scope __candidatefromlast
__candidatefromlast="$__candidatefromlast-$__nextid"
fi
fi
fi
fi
# Then we try to get the version based on the latest final one
local __candidatefromfinal
get_next_version $finalversion $scope __candidatefromfinal
if [[ -n "$identifier" ]]; then
__candidatefromfinal="$__candidatefromfinal-$identifier.1"
fi
# Finally we compare both candidates
local __resultversion
local __result
compare_versions $__candidatefromlast $__candidatefromfinal __result
case $__result in
0)
__resultversion=$__candidatefromlast
;;
-1)
__resultversion="$__candidatefromfinal"
;;
1)
__resultversion=$__candidatefromlast
;;
esac
printf -v "$1" '%s' "$__resultversion"
}
function increase_version {
local __version=""
if [ -z "${forcedversion:-}" ]; then
bump_version __version
else
if [[ "$forcedversion" =~ $SEMVER_REGEX ]] ; then
local __result
compare_versions "$forcedversion" "$lastversion" __result
if [ "$__result" -le 0 ]; then
error_exit "Version '$forcedversion' can't be lower than or equal to last version: $lastversion"
fi
else
error_exit "Invalid version format: $forcedversion"
fi
__version="$forcedversion"
fi
# Add the tag_prefix to the final version for output or tagging
__version="${tag_prefix}${__version}"
if [ "$displayonly" == "true" ]; then
echo "$__version"
else
if [ "$forcetag" == "false" ]; then
check_git_dirty_status
fi
local __commitlist
if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then
if ! __commitlist=$(git log --pretty=oneline 2>/dev/null); then
error_exit "Failed to get git commit log"
fi
else
if ! __commitlist=$(git log --pretty=oneline "$tag_prefix$finalversion"... 2>/dev/null); then
error_exit "Failed to get git commit log from $tag_prefix$finalversion"
fi
fi
# If we are forcing a bump, we add bump to the commit list
if [[ -z "$__commitlist" && "$forcetag" == "true" ]]; then
__commitlist="bump"
fi
if [[ -z "$__commitlist" ]]; then
echo "No commits since the last final version, not bumping version"
else
if [[ -z "${versionname:-}" ]]; then
if ! versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null); then
error_exit "Failed to generate timestamp for version name"
fi
fi
local __message="$versionname
$__commitlist"
# We check we have info on the user
local __username
if ! __username=$(git config user.name 2>/dev/null); then
if ! __username=$(id -u -n 2>/dev/null); then
error_exit "Failed to get username"
fi
if ! git config user.name "$__username" 2>/dev/null; then
error_exit "Failed to set git user.name"
fi
fi
local __useremail
if ! __useremail=$(git config user.email 2>/dev/null); then
if ! __useremail=$(hostname 2>/dev/null); then
error_exit "Failed to get hostname for email"
fi
if ! git config user.email "$__username@$__useremail" 2>/dev/null; then
error_exit "Failed to set git user.email"
fi
fi
if ! git tag -a "$__version" -m "$__message" 2>/dev/null; then
error_exit "Failed to create git tag: $__version"
fi
# If we have a remote, we push there
local __remotes
if __remotes=$(git remote 2>/dev/null) && [[ -n "$__remotes" ]]; then
while IFS= read -r __remote; do
if git push "$__remote" "$__version" >/dev/null 2>&1; then
echo "$__version pushed to $__remote"
else
error_exit "Failed to push tag $__version to remote $__remote"
fi
done <<< "$__remotes"
else
echo "$__version"
fi
fi
fi
}
function check_git_dirty_status {
local __repostatus=""
get_work_tree_status __repostatus
if [ "$__repostatus" == "uncommitted" ]; then
echo "ERROR: You have uncommitted changes" >&2
if git status --porcelain 2>/dev/null; then
: # status displayed successfully
else
echo "Unable to display git status" >&2
fi
exit 1
fi
if [ "$__repostatus" == "unstaged" ]; then
echo "ERROR: You have unstaged changes" >&2
if git status --porcelain 2>/dev/null; then
: # status displayed successfully
else
echo "Unable to display git status" >&2
fi
exit 1
fi
}
# Get the total amount of lines of code in the repo
function get_total_lines {
local __empty_id
if ! __empty_id=$(git hash-object -t tree /dev/null 2>/dev/null); then
error_exit "Failed to get empty tree hash"
fi
local __changes
if ! __changes=$(git diff --numstat "$__empty_id" 2>/dev/null); then
error_exit "Failed to get git diff numstat"
fi
local __added_deleted="$1"
get_changed_lines "$__changes" "$__added_deleted"
}
# Get the total amount of lines of code since the provided tag
function get_sincetag_lines {
local __sincetag="$1"
local __changes
if ! __changes=$(git diff --numstat "$__sincetag" 2>/dev/null); then
error_exit "Failed to get git diff numstat since $__sincetag"
fi
local __added_deleted="$2"
get_changed_lines "$__changes" "$__added_deleted"
}
function get_changed_lines {
local __changes_numstat="$1"
local __result="$2"
local __changes_array
if [[ -n "$__changes_numstat" ]]; then
IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat" || true
else
__changes_array=()
fi
local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$"
local __total_added=0
local __total_deleted=0
for i in "${__changes_array[@]}"
do
if [[ "$i" =~ $__diff_regex ]] ; then
local __added="${BASH_REMATCH[1]}"
local __deleted="${BASH_REMATCH[2]}"
__total_added=$(( __total_added + __added ))
__total_deleted=$(( __total_deleted + __deleted ))
fi
done
eval "$__result=( $__total_added $__total_deleted )"
}
function get_scope_auto {
local __result="$1"
local __verbose="${2:-}"
local __total=()
local __since=()
local __scope=""
get_total_lines __total
get_sincetag_lines "$tag_prefix$finalversion" __since
local __percentage=0
if [ "${__total[0]}" != "0" ]; then
__percentage=$(( 100 * ${__since[0]} / ${__total[0]} ))
if [ "$__percentage" -gt "10" ]; then
__scope="minor"
else
__scope="patch"
fi
else
__scope="patch"
fi
printf -v "$__result" '%s' "$__scope"
if [[ -n "$__verbose" ]]; then
echo "[Auto Scope] Percentage of lines changed: $__percentage"
echo "[Auto Scope] : $__scope"
fi
}
function get_work_tree_status {
local __result="$1"
# Update the index
if ! git update-index -q --ignore-submodules --refresh >/dev/null 2>&1; then
error_exit "Failed to update git index"
fi
printf -v "$__result" '%s' ""
if ! git diff-files --quiet --ignore-submodules -- >/dev/null 2>&1; then
printf -v "$__result" '%s' "unstaged"
fi
if ! git diff-index --cached --quiet HEAD --ignore-submodules -- >/dev/null 2>&1; then
printf -v "$__result" '%s' "uncommitted"
fi
}
function get_current {
local __result="$1"
local __commitcount
if [ "$hasversiontag" == "true" ]; then
if ! __commitcount=$(git rev-list "$tag_prefix$lastversion".. --count 2>/dev/null); then
error_exit "Failed to get commit count from $tag_prefix$lastversion"
fi
else
if ! __commitcount=$(git rev-list --count HEAD 2>/dev/null); then
error_exit "Failed to get total commit count"
fi
fi
local __status=""
get_work_tree_status __status
if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then
printf -v "$__result" '%s' "$lastversion"
else
local __buildinfo
if ! __buildinfo=$(git rev-parse --short HEAD 2>/dev/null); then
error_exit "Failed to get short commit hash"
fi
local __currentbranch
if ! __currentbranch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); then
error_exit "Failed to get current branch name"
fi
# Safely truncate branch name
__currentbranch="${__currentbranch:0:$MAX_BRANCH_LENGTH}"
local default_branch
get_default_branch default_branch
if [ "$__currentbranch" != "$default_branch" ]; then
__buildinfo="$__currentbranch.$__buildinfo"
fi
local __suffix=""
if [ "$__commitcount" != "0" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__commitcount"
fi
if [ -n "$__status" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__status"
fi
__suffix="$__suffix+$__buildinfo"
if [ "$lastversion" == "$finalversion" ]; then
scope="patch"
identifier=""
local __bumped=""
bump_version __bumped
printf -v "$__result" '%s' "$__bumped-dev.$__suffix"
else
printf -v "$__result" '%s' "$lastversion.$__suffix"
fi
fi
}
function init {
check_git_repo
local TAGS
if ! TAGS=$(git tag --merged 2>/dev/null); then
error_exit "Failed to get git tags"
fi
local TAG_ARRAY
if [[ -n "$TAGS" ]]; then
IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS" || true
else
TAG_ARRAY=()
fi
# Filter and strip tag_prefix from tags
local FILTERED_TAGS=()
if [[ ${#TAG_ARRAY[@]} -gt 0 ]]; then
for tag in "${TAG_ARRAY[@]}"; do
if [[ "$tag" == "$tag_prefix"* ]]; then
# Strip the tag_prefix from the tag
local stripped_tag="${tag#$tag_prefix}"
FILTERED_TAGS+=("$stripped_tag")
fi
done
fi
if [[ ${#FILTERED_TAGS[@]} -gt 0 ]]; then
get_latest "${FILTERED_TAGS[@]}"
else
get_latest
fi
if ! currentbranch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); then
error_exit "Failed to get current git branch"
fi
}
case "$ACTION" in
--help)
echo -e "$HELP"
;;
--version)
echo -e "${PROG}: $PROG_VERSION"
;;
final)
init
default_branch=""
get_default_branch default_branch
diff=""
if ! diff=$(git diff "$default_branch" 2>/dev/null); then
error_exit "Failed to compare with default branch: $default_branch"
fi
if [ "$forcetag" == "false" ]; then
if [ -n "$diff" ]; then
error_exit "Branch must be updated with $default_branch for final versions"
fi
fi
increase_version
;;
alpha|beta)
init
identifier="$ACTION"
increase_version
;;
candidate)
init
identifier="rc"
increase_version
;;
getlast)
init
echo "$tag_prefix$lastversion"
;;
getfinal)
init
echo "$tag_prefix$finalversion"
;;
getcurrent)
init
current=""
get_current current
echo "$current"
;;
get)
init
echo "Current final version: $tag_prefix$finalversion"
echo "Last tagged version: $tag_prefix$lastversion"
;;
*)
error_exit "'$ACTION' is not a valid command, see --help for available commands."
;;
esac
================================================
FILE: semtag_bkp
================================================
#!/usr/bin/env bash
# This is a deprecated version
PROG=semtag
PROG_VERSION="v0.1.2"
SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"
IDENTIFIER_REGEX="^\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$"
NUMERIC_REGEX='^[0-9]+$'
# Length limit for the branch name in build metadata.
MAX_BRANCH_LENGTH=50
# Global variables
FIRST_VERSION="v0.0.0"
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
hasversiontag="false"
scope="patch"
displayonly="false"
forcetag="false"
prefix="v"
forcedversion=
versionname=
identifier=
HELP="\
Usage:
$PROG
$PROG getlast
$PROG getfinal
$PROG (final|alpha|beta|candidate) [-s <scope> (major|minor|patch|auto) | -o]
$PROG --help
$PROG --version
Options:
-s The scope that must be increased, can be major, minor or patch.
The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD)
where X, Y and Z are positive integers, PRERELEASE is an optionnal
string composed of alphanumeric characters describing if the build is
a release candidate, alpha or beta version, with a number.
BUILD is also an optional string composed of alphanumeric
characters and hyphens.
Setting the scope as 'auto', the script will chose the scope between
'minor' and 'patch', depending on the amount of lines added (<10% will
choose patch).
-v Specifies manually the version to be tagged, must be a valid semantic version
in the format X.Y.Z where X, Y and Z are positive integers.
-o Output the version only, shows the bumped version, but doesn't tag.
-f Forces to tag, even if there are unstaged or uncommited changes.
-p Use a plain version, ie. do not prefix with 'v'.
Commands:
--help Print this help message.
--version Prints the program's version.
get Returns both current final version and last tagged version.
getlast Returns the latest tagged version.
getfinal Returns the latest tagged final version.
getcurrent Returns the current version, based on the latest one, if there are uncommited or
unstaged changes, they will be reflected in the version, adding the number of
pending commits, current branch and commit hash.
final Tags the current build as a final version, this only can be done on the default branch.
candidate Tags the current build as a release candidate, the tag will contain all
the commits from the last final version.
alpha Tags the current build as an alpha version, the tag will contain all
the commits from the last final version.
beta Tags the current build as a beta version, the tag will contain all
the commits from the last final version."
# Commands and options
ACTION="getlast"
ACTION="$1"
shift
# We get the parameters
while getopts "v:s:ofp" opt; do
case $opt in
v)
forcedversion="$OPTARG"
;;
s)
scope="$OPTARG"
;;
o)
displayonly="true"
;;
f)
forcetag="true"
;;
p)
prefix=""
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
# Try to programmatically fetch the default branch. Go by the first remote HEAD found, otherwise default to `master`.
# $1 The variable to store the result
function get_default_branch {
local __result=$1
local __remotes=$(git remote)
if [[ -n $__remotes ]]; then
for __remote in $__remotes; do
local __default_branch_ref=$(git symbolic-ref --quiet refs/remotes/${__remote}/HEAD || true)
local __default_branch=${__default_branch_ref#refs/remotes/${__remote}/}
if [[ -n ${__default_branch} ]]; then
break
fi
done
fi
eval "${__result}=${__default_branch:-master}"
}
# Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version
# $1 The string containing the version in semantic format
# $2 The variable to store the result array:
# position 0: major number
# position 1: minor number
# position 2: patch number
# position 3: identifier (or prerelease identifier)
# position 4: build info
function explode_version {
local __version=$1
local __result=$2
if [[ $__version =~ $SEMVER_REGEX ]] ; then
local __major=${BASH_REMATCH[1]}
local __minor=${BASH_REMATCH[2]}
local __patch=${BASH_REMATCH[3]}
local __prere=${BASH_REMATCH[4]}
local __build=${BASH_REMATCH[5]}
eval "$__result=(\"$__major\" \"$__minor\" \"$__patch\" \"$__prere\" \"$__build\")"
else
eval "$__result="
fi
}
# Compare two versions and returns -1, 0 or 1
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the result
function compare_versions {
local __first
local __second
explode_version $1 __first
explode_version $2 __second
local lv=$3
# Compares MAJOR, MINOR and PATCH
for i in 0 1 2; do
local __numberfirst=${__first[$i]}
local __numbersecond=${__second[$i]}
case $(($__numberfirst - $__numbersecond)) in
0)
;;
-[0-9]*)
eval "$lv=-1"
return 0
;;
[0-9]*)
eval "$lv=1"
return 0
;;
esac
done
# Identifiers should compare with the ASCII order.
local __identifierfirst=${__first[3]}
local __identifiersecond=${__second[3]}
compare_identifiers "${__first[3]}" "${__second[3]}" compareresult
eval "$lv=$compareresult"
}
# Returns the number comparison
# $1 The first number to compare
# $2 The second number to compare
# $3 The variable where to store the result
function compare_numeric {
local __first=$1
local __second=$2
local __result=$3
if (( "$__first" < "$__second" )) ; then
eval "$__result=-1"
elif (( "$__first" > "$__second" )) ; then
eval "$__result=1"
else
eval "$__result=0"
fi
}
# Returns the alpanumeric comparison
# $1 The first alpanumeric to compare
# $2 The second alpanumeric to compare
# $3 The variable where to store the result
function compare_alphanumeric {
local __first=$1
local __second=$2
local __result=$3
if [[ "$__first" < "$__second" ]] ; then
eval "$__result=-1"
elif [[ "$__first" > "$__second" ]] ; then
eval "$__result=1"
else
eval "$__result=0"
fi
}
# Returns the last version of two
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the last one
function get_latest_of_two {
local __first=$1
local __second=$2
local __result
local __latest=$3
compare_versions $__first $__second __result
case $__result in
0)
eval "$__latest=$__second"
;;
-1)
eval "$__latest=$__second"
;;
1)
eval "$__latest=$__first"
;;
esac
}
# Returns comparison of two identifier parts
# $1 The first part to compare
# $2 The second part to compare
# $3 The variable where to store the compare result
function compare_identifier_part {
local __first=$1
local __second=$2
local __result=$3
local compareresult
if [[ "$__first" =~ $NUMERIC_REGEX ]] && [[ "$__second" =~ $NUMERIC_REGEX ]] ; then
compare_numeric "$__first" "$__second" compareresult
eval "$__result=$compareresult"
return 0
fi
compare_alphanumeric "$__first" "$__second" compareresult
eval "$__result=$compareresult"
}
# Returns comparison of two identifiers
# $1 The first identifier to compare
# $2 The second identifier to compare
# $3 The variable where to store the compare result
function compare_identifiers {
local __first=$1
local __second=$2
local __result=$3
local partresult
local arraylengths
if [[ -n "$__first" ]] && [[ -n "$__second" ]]; then
explode_identifier "${__first}" explodedidentifierfirst
explode_identifier "${__second}" explodedidentifiersecond
firstsize=${#explodedidentifierfirst[@]}
secondsize=${#explodedidentifiersecond[@]}
minlength=$(( $firstsize<$secondsize ? $firstsize : $secondsize ))
for (( i = 0 ; i < $minlength ; i++ )); do
compare_identifier_part "${explodedidentifierfirst[$i]}" "${explodedidentifiersecond[$i]}" partresult
case $partresult in
0)
;;
*)
eval "$__result=$partresult"
return 0
;;
esac
done
compare_numeric $firstsize $secondsize arraylengths
eval "$__result=$arraylengths"
return 0
elif [[ -z "$__first" ]] && [[ -n "$__second" ]]; then
eval "$__result=1"
return 0
elif [[ -n "$__first" ]] && [[ -z "$__second" ]]; then
eval "$__result=-1"
return 0
fi
eval "$__result=0"
}
# Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1
# $1 The identifier in the format -id.#
# $2 The vferiable where to store the 2 size array
function explode_identifier {
local __identifier=$1
local __result=$2
if [[ $__identifier =~ $IDENTIFIER_REGEX ]] ; then
IFS='-.' read -ra identifierparts <<< $__identifier
eval "$__result=( ${identifierparts[@]} )"
else
eval "$__result="
fi
}
# Gets a list of tags and assigns the base and latest versions
# Receives an array with the tags containing the versions
# Assigns to the global variables finalversion and lastversion the final version and the latest version
function get_latest {
local __taglist=("$@")
local __tagsnumber=${#__taglist[@]}
local __current
case $__tagsnumber in
0)
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
;;
1)
__current=${__taglist[0]}
explode_version $__current ver
if [ -n "$ver" ]; then
if [ -n "${ver[3]}" ]; then
finalversion=$FIRST_VERSION
else
finalversion=$__current
fi
lastversion=$__current
else
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
fi
;;
*)
local __lastpos=$(($__tagsnumber-1))
for i in $(seq 0 $__lastpos)
do
__current=${__taglist[i]}
explode_version ${__taglist[i]} ver
if [ -n "$ver" ]; then
if [ -z "${ver[3]}" ]; then
get_latest_of_two $finalversion $__current finalversion
get_latest_of_two $lastversion $finalversion lastversion
else
get_latest_of_two $lastversion $__current lastversion
fi
fi
done
;;
esac
if git rev-parse -q --verify "refs/tags/$lastversion" >/dev/null; then
hasversiontag="true"
else
hasversiontag="false"
fi
}
# Gets the next version given the provided scope
# $1 The version that is going to be bumped
# $2 The scope to bump
# $3 The variable where to stoer the result
function get_next_version {
local __exploded
local __fromversion=$1
local __scope=$2
local __result=$3
explode_version $__fromversion __exploded
case $__scope in
major)
__exploded[0]=$((${__exploded[0]}+1))
__exploded[1]=0
__exploded[2]=0
;;
minor)
__exploded[1]=$((${__exploded[1]}+1))
__exploded[2]=0
;;
patch)
__exploded[2]=$((${__exploded[2]}+1))
;;
esac
eval "$__result=${prefix}${__exploded[0]}.${__exploded[1]}.${__exploded[2]}"
}
function bump_version {
## First we try to get the next version based on the existing last one
if [ "$scope" == "auto" ]; then
get_scope_auto scope
fi
local __candidatefromlast=$FIRST_VERSION
local __explodedlast
explode_version $lastversion __explodedlast
if [[ -n "${__explodedlast[3]}" ]]; then
# Last version is not final
local __idlast
explode_identifier ${__explodedlast[3]} __idlast
# We get the last, given the desired id based on the scope
__candidatefromlast="${prefix}${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}"
if [[ -n "$identifier" ]]; then
local __nextid="$identifier.1"
if [ "$identifier" == "${__idlast[0]}" ]; then
# We target the same identifier as the last so we increase one
__nextid="$identifier.$(( ${__idlast[1]}+1 ))"
__candidatefromlast="$__candidatefromlast-$__nextid"
else
# Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version
__candidatefromlast="$__candidatefromlast-$__nextid"
local __comparedwithlast
compare_versions $__candidatefromlast $lastversion __comparedwithlast
if [ "$__comparedwithlast" == -1 ]; then
get_next_version $__candidatefromlast $scope __candidatefromlast
__candidatefromlast="$__candidatefromlast-$__nextid"
fi
fi
fi
fi
# Then we try to get the version based on the latest final one
local __candidatefromfinal=$FIRST_VERSION
get_next_version $finalversion $scope __candidatefromfinal
if [[ -n "$identifier" ]]; then
__candidatefromfinal="$__candidatefromfinal-$identifier.1"
fi
# Finally we compare both candidates
local __resultversion
local __result
compare_versions $__candidatefromlast $__candidatefromfinal __result
case $__result in
0)
__resultversion=$__candidatefromlast
;;
-1)
__resultversion="$__candidatefromfinal"
;;
1)
__resultversion=$__candidatefromlast
;;
esac
eval "$1=$__resultversion"
}
function increase_version {
local __version=
if [ -z $forcedversion ]; then
bump_version __version
else
if [[ $forcedversion =~ $SEMVER_REGEX ]] ; then
compare_versions $forcedversion $lastversion __result
if [ $__result -le 0 ]; then
echo "Version can't be lower than last version: $lastversion"
exit 1
fi
else
echo "Non valid version to bump"
exit 1
fi
__version=$forcedversion
fi
if [ "$displayonly" == "true" ]; then
echo "$__version"
else
if [ "$forcetag" == "false" ]; then
check_git_dirty_status
fi
local __commitlist
if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then
__commitlist="$(git log --pretty=oneline | cat)"
else
__commitlist="$(git log --pretty=oneline $finalversion... | cat)"
fi
# If we are forcing a bump, we add bump to the commit list
if [[ -z $__commitlist && "$forcetag" == "true" ]]; then
__commitlist="bump"
fi
if [[ -z $__commitlist ]]; then
echo "No commits since the last final version, not bumping version"
else
if [[ -z $versionname ]]; then
versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
fi
local __message="$versionname
$__commitlist"
# We check we have info on the user
local __username=$(git config user.name)
if [ -z "$__username" ]; then
__username=$(id -u -n)
git config user.name $__username
fi
local __useremail=$(git config user.email)
if [ -z "$__useremail" ]; then
__useremail=$(hostname)
git config user.email "$__username@$__useremail"
fi
git tag -a $__version -m "$__message"
# If we have a remote, we push there
local __remotes=$(git remote)
if [[ -n $__remotes ]]; then
for __remote in $__remotes; do
git push $__remote $__version > /dev/null
if [ $? -eq 0 ]; then
echo "$__version pushed to $__remote"
else
echo "Error pushing the tag $__version to $__remote"
exit 1
fi
done
else
echo "$__version"
fi
fi
fi
}
function check_git_dirty_status {
local __repostatus=
get_work_tree_status __repostatus
if [ "$__repostatus" == "uncommitted" ]; then
echo "ERROR: You have uncommitted changes"
git status --porcelain
exit 1
fi
if [ "$__repostatus" == "unstaged" ]; then
echo "ERROR: You have unstaged changes"
git status --porcelain
exit 1
fi
}
# Get the total amount of lines of code in the repo
function get_total_lines {
local __empty_id="$(git hash-object -t tree /dev/null)"
local __changes="$(git diff --numstat $__empty_id | cat)"
local __added_deleted=$1
get_changed_lines "$__changes" $__added_deleted
}
# Get the total amount of lines of code since the provided tag
function get_sincetag_lines {
local __sincetag=$1
local __changes="$(git diff --numstat $__sincetag | cat)"
local __added_deleted=$2
get_changed_lines "$__changes" $__added_deleted
}
function get_changed_lines {
local __changes_numstat=$1
local __result=$2
IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat"
local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$"
local __total_added=0
local __total_deleted=0
for i in "${__changes_array[@]}"
do
if [[ $i =~ $__diff_regex ]] ; then
local __added=${BASH_REMATCH[1]}
local __deleted=${BASH_REMATCH[2]}
__total_added=$(( $__total_added+$__added ))
__total_deleted=$(( $__total_deleted+$__deleted ))
fi
done
eval "$2=( $__total_added $__total_deleted )"
}
function get_scope_auto {
local __verbose=$2
local __total=0
local __since=0
local __scope=
get_total_lines __total
get_sincetag_lines $finalversion __since
local __percentage=0
if [ "$__total" != "0" ]; then
local __percentage=$(( 100*$__since/$__total ))
if [ $__percentage -gt "10" ]; then
__scope="minor"
else
__scope="patch"
fi
fi
eval "$1=$__scope"
if [[ -n "$__verbose" ]]; then
echo "[Auto Scope] Percentage of lines changed: $__percentage"
echo "[Auto Scope] : $__scope"
fi
}
function get_work_tree_status {
# Update the index
git update-index -q --ignore-submodules --refresh > /dev/null
eval "$1="
if ! git diff-files --quiet --ignore-submodules -- > /dev/null
then
eval "$1=unstaged"
fi
if ! git diff-index --cached --quiet HEAD --ignore-submodules -- > /dev/null
then
eval "$1=uncommitted"
fi
}
function get_current {
if [ "$hasversiontag" == "true" ]; then
local __commitcount="$(git rev-list $lastversion.. --count)"
else
local __commitcount="$(git rev-list --count HEAD)"
fi
local __status=
get_work_tree_status __status
if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then
eval "$1=$lastversion"
else
local __buildinfo="$(git rev-parse --short HEAD)"
local __currentbranch="$(git rev-parse --abbrev-ref HEAD | cut -c1-$MAX_BRANCH_LENGTH)"
get_default_branch default_branch
if [ "$__currentbranch" != "master" ]; then
__buildinfo="$__currentbranch.$__buildinfo"
fi
local __suffix=
if [ "$__commitcount" != "0" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__commitcount"
fi
if [ -n "$__status" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__status"
fi
__suffix="$__suffix+$__buildinfo"
if [ "$lastversion" == "$finalversion" ]; then
scope="patch"
identifier=
local __bumped=
bump_version __bumped
eval "$1=$__bumped-dev.$__suffix"
else
eval "$1=$lastversion.$__suffix"
fi
fi
}
function init {
TAGS="$(git tag --merged)"
IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS"
get_latest ${TAG_ARRAY[@]}
currentbranch="$(git rev-parse --abbrev-ref HEAD)"
}
case $ACTION in
--help)
echo -e "$HELP"
;;
--version)
echo -e "${PROG}: $PROG_VERSION"
;;
final)
init
get_default_branch default_branch
diff=$(git diff $default_branch | cat)
if [ "$forcetag" == "false" ]; then
if [ -n "$diff" ]; then
echo "ERROR: Branch must be updated with $default_branch for final versions"
exit 1
fi
fi
increase_version
;;
alpha|beta)
init
identifier="$ACTION"
increase_version
;;
candidate)
init
identifier="rc"
increase_version
;;
getlast)
init
echo "$lastversion"
;;
getfinal)
init
echo "$finalversion"
;;
getcurrent)
init
get_current current
echo "$current"
;;
get)
init
echo "Current final version: $finalversion"
echo "Last tagged version: $lastversion"
;;
*)
echo "'$ACTION' is not a valid command, see --help for available commands."
;;
esac
================================================
FILE: test_semtag.sh
================================================
#!/usr/bin/env bash
# Test script for semtag beta/alpha/rc increment functionality
# This script validates that pre-release version increments work correctly
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SEMTAG="$SCRIPT_DIR/semtag"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# Array to store created tags for cleanup
CREATED_TAGS=()
# Function to print test results
print_result() {
local test_name="$1"
local expected="$2"
local actual="$3"
TESTS_RUN=$((TESTS_RUN + 1))
if [ "$expected" = "$actual" ]; then
echo -e "${GREEN}✓${NC} $test_name"
echo " Expected: $expected"
echo " Got: $actual"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}✗${NC} $test_name"
echo " Expected: $expected"
echo " Got: $actual"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo
}
# Function to create a test tag
create_tag() {
local tag="$1"
git tag "$tag" -m "test tag: $tag" >/dev/null 2>&1
CREATED_TAGS+=("$tag")
}
# Function to delete test tags
cleanup_tags() {
if [ ${#CREATED_TAGS[@]} -gt 0 ]; then
echo -e "${YELLOW}Cleaning up test tags...${NC}"
for tag in "${CREATED_TAGS[@]}"; do
git tag -d "$tag" >/dev/null 2>&1 || true
done
CREATED_TAGS=()
fi
}
# Cleanup on exit
trap cleanup_tags EXIT
echo "================================================"
echo "Semtag Pre-release Version Increment Tests"
echo "================================================"
echo
# Get the current state
INITIAL_LAST=$("$SEMTAG" getlast)
INITIAL_FINAL=$("$SEMTAG" getfinal)
echo "Initial state:"
echo " Last version: $INITIAL_LAST"
echo " Final version: $INITIAL_FINAL"
echo
# Test 1: Beta with patch scope - first beta
echo "=== Test 1: Beta Patch Increment ==="
result=$("$SEMTAG" beta -s patch -o)
print_result "First beta with patch scope" "v0.2.1-beta.1" "$result"
# Test 2: Beta with patch scope - increment beta number
create_tag "v0.2.1-beta.1"
result=$("$SEMTAG" beta -s patch -o)
print_result "Increment beta.1 to beta.2" "v0.2.1-beta.2" "$result"
# Test 3: Beta with patch scope - increment beta number again
create_tag "v0.2.1-beta.2"
result=$("$SEMTAG" beta -s patch -o)
print_result "Increment beta.2 to beta.3" "v0.2.1-beta.3" "$result"
cleanup_tags
# Test 4: Beta with minor scope - first beta
echo "=== Test 2: Beta Minor Increment ==="
result=$("$SEMTAG" beta -s minor -o)
print_result "First beta with minor scope" "v0.3.0-beta.1" "$result"
# Test 5: Beta with minor scope - increment beta number
create_tag "v0.3.0-beta.1"
result=$("$SEMTAG" beta -s minor -o)
print_result "Increment beta.1 to beta.2 (minor)" "v0.3.0-beta.2" "$result"
# Test 6: Beta with minor scope - increment beta number again
create_tag "v0.3.0-beta.2"
result=$("$SEMTAG" beta -s minor -o)
print_result "Increment beta.2 to beta.3 (minor)" "v0.3.0-beta.3" "$result"
cleanup_tags
# Test 7: Beta with major scope - first beta
echo "=== Test 3: Beta Major Increment ==="
result=$("$SEMTAG" beta -s major -o)
print_result "First beta with major scope" "v1.0.0-beta.1" "$result"
# Test 8: Beta with major scope - increment beta number
create_tag "v1.0.0-beta.1"
result=$("$SEMTAG" beta -s major -o)
print_result "Increment beta.1 to beta.2 (major)" "v1.0.0-beta.2" "$result"
cleanup_tags
# Test 9: Alpha increments
echo "=== Test 4: Alpha Increment ==="
result=$("$SEMTAG" alpha -s patch -o)
print_result "First alpha with patch scope" "v0.2.1-alpha.1" "$result"
create_tag "v0.2.1-alpha.1"
result=$("$SEMTAG" alpha -s patch -o)
print_result "Increment alpha.1 to alpha.2" "v0.2.1-alpha.2" "$result"
cleanup_tags
# Test 10: Release candidate increments
echo "=== Test 5: Release Candidate Increment ==="
result=$("$SEMTAG" candidate -s patch -o)
print_result "First rc with patch scope" "v0.2.1-rc.1" "$result"
create_tag "v0.2.1-rc.1"
result=$("$SEMTAG" candidate -s patch -o)
print_result "Increment rc.1 to rc.2" "v0.2.1-rc.2" "$result"
cleanup_tags
# Test 11: Switching between identifiers
echo "=== Test 6: Identifier Switching ==="
create_tag "v0.3.0-alpha.5"
result=$("$SEMTAG" beta -s minor -o)
print_result "Switch from alpha to beta (same version)" "v0.3.0-beta.1" "$result"
create_tag "v0.3.0-beta.1"
result=$("$SEMTAG" alpha -s minor -o)
print_result "Switch from beta to alpha (bumps version)" "v0.4.0-alpha.1" "$result"
cleanup_tags
# Test 12: Multiple sequential increments
echo "=== Test 7: Sequential Beta Increments ==="
create_tag "v0.2.1-beta.1"
create_tag "v0.2.1-beta.2"
create_tag "v0.2.1-beta.3"
result=$("$SEMTAG" beta -s patch -o)
print_result "Increment beta.3 to beta.4" "v0.2.1-beta.4" "$result"
cleanup_tags
# Test 13: Final version with patch scope
echo "=== Test 8: Final Version Patch Increment ==="
# Start from v0.2.0, increment patch
result=$("$SEMTAG" final -s patch -o -f)
print_result "Final version patch increment" "v0.2.1" "$result"
# Test 14: Final version with minor scope
echo "=== Test 9: Final Version Minor Increment ==="
result=$("$SEMTAG" final -s minor -o -f)
print_result "Final version minor increment" "v0.3.0" "$result"
# Test 15: Final version with major scope
echo "=== Test 10: Final Version Major Increment ==="
result=$("$SEMTAG" final -s major -o -f)
print_result "Final version major increment" "v1.0.0" "$result"
# Test 16: Sequential final versions
echo "=== Test 11: Sequential Final Version Increments ==="
create_tag "v0.2.1"
result=$("$SEMTAG" final -s patch -o -f)
print_result "Increment v0.2.1 to v0.2.2" "v0.2.2" "$result"
create_tag "v0.2.2"
result=$("$SEMTAG" final -s patch -o -f)
print_result "Increment v0.2.2 to v0.2.3" "v0.2.3" "$result"
cleanup_tags
# Test 17: Going from pre-release to final
echo "=== Test 12: Pre-release to Final Version ==="
create_tag "v0.3.0-beta.3"
result=$("$SEMTAG" final -s minor -o -f)
print_result "From beta.3 to final minor version" "v0.3.0" "$result"
create_tag "v0.4.0-rc.2"
result=$("$SEMTAG" final -s minor -o -f)
print_result "From rc.2 to final minor version" "v0.4.0" "$result"
cleanup_tags
# Test 18: Mixed major/minor/patch with pre-releases
echo "=== Test 13: Mixed Version Scenarios ==="
create_tag "v1.5.3"
result=$("$SEMTAG" beta -s patch -o)
print_result "From v1.5.3 to beta patch" "v1.5.4-beta.1" "$result"
create_tag "v1.5.4-beta.1"
create_tag "v1.5.4-beta.2"
result=$("$SEMTAG" final -s patch -o -f)
print_result "From beta.2 to final patch" "v1.5.4" "$result"
cleanup_tags
# Test 19: Major version with pre-releases
echo "=== Test 14: Major Version with Pre-releases ==="
create_tag "v2.0.0"
result=$("$SEMTAG" beta -s major -o)
print_result "From v2.0.0 to major beta" "v3.0.0-beta.1" "$result"
create_tag "v3.0.0-beta.1"
result=$("$SEMTAG" final -s major -o -f)
print_result "From beta.1 to final major" "v3.0.0" "$result"
cleanup_tags
# Print summary
echo "================================================"
echo "Test Summary"
echo "================================================"
echo -e "Tests run: $TESTS_RUN"
echo -e "${GREEN}Tests passed: $TESTS_PASSED${NC}"
if [ $TESTS_FAILED -gt 0 ]; then
echo -e "${RED}Tests failed: $TESTS_FAILED${NC}"
else
echo -e "${GREEN}Tests failed: $TESTS_FAILED${NC}"
fi
echo "================================================"
# Exit with failure if any tests failed
if [ $TESTS_FAILED -gt 0 ]; then
exit 1
else
echo -e "${GREEN}All tests passed!${NC}"
exit 0
fi
gitextract_v776qutd/ ├── .gitignore ├── LICENSE ├── README.md ├── release.sh ├── semtag ├── semtag_bkp └── test_semtag.sh
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (87K chars).
[
{
"path": ".gitignore",
"chars": 170,
"preview": "### Vim ###\n# swap\n[._]*.s[a-w][a-z]\n[._]s[a-w][a-z]\n# session\nSession.vim\n# temporary\n.netrwhist\n*~\n# auto-generated ta"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 8574,
"preview": "# Semtag\n\nSemantic Tagging Script for Git\n\n[Version: v0.2.0]\n\nNotes: *This script is inspired by the [Nebula Release Plu"
},
{
"path": "release.sh",
"chars": 8404,
"preview": "#!/usr/bin/env bash\n\n# release.sh - Release automation script (Refactored)\n# Improved version with better error handling"
},
{
"path": "semtag",
"chars": 25716,
"preview": "#!/usr/bin/env bash\n\n# Enable strict error handling\nset -euo pipefail\n\nPROG=semtag\nPROG_VERSION=\"v0.1.2\"\n\n# Error handli"
},
{
"path": "semtag_bkp",
"chars": 20525,
"preview": "#!/usr/bin/env bash\n\n# This is a deprecated version\n\nPROG=semtag\nPROG_VERSION=\"v0.1.2\"\n\nSEMVER_REGEX=\"^v?(0|[1-9][0-9]*)"
},
{
"path": "test_semtag.sh",
"chars": 7585,
"preview": "#!/usr/bin/env bash\n\n# Test script for semtag beta/alpha/rc increment functionality\n# This script validates that pre-rel"
}
]
About this extraction
This page contains the full source code of the nico2sh/semtag GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (80.4 KB), approximately 22.1k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.