Full Code of nico2sh/semtag for AI

master 1600023e1c3a cached
7 files
80.4 KB
22.1k tokens
1 requests
Download .txt
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
Download .txt
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.

Copied to clipboard!