File: release_helper.sh

package info (click to toggle)
xournalpp 1.3.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 30,044 kB
  • sloc: cpp: 64,195; xml: 939; sh: 752; ansic: 362; python: 338; php: 74; makefile: 15
file content (594 lines) | stat: -rwxr-xr-x 23,048 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
#!/usr/bin/env bash
# shellcheck disable=SC2155

set -o pipefail

# Get the path of the script
SCRIPT_PATH=$(dirname "$(realpath -s "$0")")

# Parse current version string
function current_major_version() {
    sed -n 's/^        VERSION \([0-9]\+\).\([0-9]\+\).\([0-9]\+\)$/\1/p' "${SCRIPT_PATH}/../CMakeLists.txt"
}

function current_minor_version() {
    sed -n 's/^        VERSION \([0-9]\+\).\([0-9]\+\).\([0-9]\+\)$/\2/p' "${SCRIPT_PATH}/../CMakeLists.txt"
}

function current_patch_version() {
    sed -n 's/^        VERSION \([0-9]\+\).\([0-9]\+\).\([0-9]\+\)$/\3/p' "${SCRIPT_PATH}/../CMakeLists.txt"
}

function current_suffix_version() {
    sed -n 's/^set\s*(VERSION_SUFFIX "\([+~][^\"]*\)")/\1/p' "${SCRIPT_PATH}/../CMakeLists.txt"
}

function current_version() {
    echo "$(current_major_version).$(current_minor_version).$(current_patch_version)$(current_suffix_version)"
}

function parse_major_version() {
    echo "$1" | sed -n 's/^\([0-9]\+\)\..*$/\1/p'
}

function parse_minor_version() {
    echo "$1" | sed -n 's/^[0-9]\+\.\([0-9]\+\)\..*$/\1/p'
}

function parse_patch_version() {
    echo "$1" | sed -n 's/^[0-9]\+\.[0-9]\+\.\([0-9]\+\).*$/\1/p'
}

function parse_suffix_version() {
    echo "$1" | sed -n 's/^[0-9]\+\.[0-9]\+\.[0-9]\+\([+~].*\)$/\1/p'
}

# Compares two versions $1 and $2
# If $2 is greater it returns 0
function compare_versions() {
    if [[ $1 == "$2" ]]; then
        return 1
    fi
    if printf "%s\n%s" "$1" "$2" | LC_ALL=C sort -CVu; then
        return 0
    fi
    return 1
}

# Validates the provided version string
# $1 the version as a string
# Returns 1 if the version is valid
function validate_version() {
    if [ -z "$1" ]; then
        return 0
    fi

    if ! [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+([+~][A-Za-z0-9+~\.-]*)*?$ ]]; then
        return 1
    else
        if [[ $1 =~ ~dev$ ]]; then
            return 0
        fi
        return 1
    fi
}

# Print information about the correct format of a version string
function print_version_help() {
    echo "Please supply the version of the release in the format [0-9]+.[0-9]+.[0-9]+([+~][0-9A-Za-z+~.-]*)*"
    echo "The suffix is optional, starts with a '+' or '~' and may only contain alphanumeric characters and '+', '~', '.' and '-'."
    echo "Within the suffix '+' is evaluated as a higher release and '~' as a lower release than the basic version number."
}

# Abort execution
# $1 The exit code
# $2 The message to print before abort
function abort() {
    echo "ABORT: $2"
    exit "$1"
}

# Check if branch already exists
# $1 The name of the branch
# If it exists returns 0
function branch_exists() {
    if git rev-parse --quiet --verify "$1" > /dev/null; then
        return 0
    fi
    return 1
}

# Check if release already exists
# $1 The version string of the release
# If it exists returns 0
function release_exists() {
    if git tag | grep -Fxq "v$1"; then
        return 0
    fi
    return 1
}

# Check if git is in detached head mode
# If yes returns 1
function is_detached() {
    if git status --branch --porcelain | grep -Fxq "## HEAD (no branch)"; then
        return 0
    fi
    return 1
}

# Bumps the version in all relevant places
# Should the prior version be the same version but with an added ~dev this version is replaced
# $1 The version string
# $2 Whether to replace the current version
function bump_version() {
    local prior_version=$(current_version)
    local replace=$2

    if ! [[ "$1" =~ ~dev|\+dev$ ]]; then
        local publish=1
    else
        local publish=0
    fi

    # Update version in the CMakeLists.txt
    sed -i "s/^        VERSION $(current_major_version).$(current_minor_version).$(current_patch_version)/        VERSION $(parse_major_version "$1").$(parse_minor_version "$1").$(parse_patch_version "$1")/" "${SCRIPT_PATH}"/../CMakeLists.txt
    sed -i "s/set\s*(VERSION_SUFFIX \"[^\"]*\")/set(VERSION_SUFFIX \"$(parse_suffix_version "$1")\")/g" "${SCRIPT_PATH}/../CMakeLists.txt"

    # From now on current_*_version functions return the new version

    # Update Changelog
    if [ "$replace" -eq 0 ]; then
        sed -i "N;N;s/# Changelog\n\n/# Changelog\n\n## $(current_version) (Unreleased)\n\n/g" "${SCRIPT_PATH}/../CHANGELOG.md"
    else
        if [ $publish -eq 0 ]; then
            sed -i "s/## ${prior_version} (Unreleased)/## $(current_version) (Unreleased)/g" "${SCRIPT_PATH}/../CHANGELOG.md"
        else
            sed -i "s/## ${prior_version} (Unreleased)/## $(current_version)/g" "${SCRIPT_PATH}/../CHANGELOG.md"
        fi
    fi

    # Update Debian Changelog
    local git_user=
    if ! git_user=$(git config --get user.name); then
        abort 2 "Could not read current git user - Please set with 'git config --set user.name <name>'"
    fi

    local git_mail=
    if ! git_mail=$(git config --get user.email); then
        abort 2 "Could not read current git user email - Please set with 'git config --set user.email <email>'"
    fi

    local date=$(date --rfc-2822)

    if [ "$replace" -eq 0 ]; then
        sed -i "1i xournalpp ($(current_version)-1) UNRELEASED; urgency=medium\n\n  * \n\n -- ${git_user} <${git_mail}>  ${date}\n" "${SCRIPT_PATH}/../debian/changelog"
    else
        if [ $publish -eq 0 ]; then
            sed -i "s/xournalpp (${prior_version}-1) UNRELEASED; urgency=medium/xournalpp ($(current_version)-1) UNRELEASED; urgency=medium/g" "${SCRIPT_PATH}/../debian/changelog"
            sed -i "/xournalpp ($(current_version)-1)/,/xournalpp/s/ --.*/ -- ${git_user} <${git_mail}>  ${date}/g" "${SCRIPT_PATH}/../debian/changelog"
        else
            sed -i "s/xournalpp (${prior_version}-1) UNRELEASED; urgency=medium/xournalpp ($(current_version)-1) unstable; urgency=medium/g" "${SCRIPT_PATH}/../debian/changelog"
            sed -i "/xournalpp ($(current_version)-1)/,/xournalpp/s/ --.*/ -- ${git_user} <${git_mail}>  ${date}/g" "${SCRIPT_PATH}/../debian/changelog"
        fi
    fi

    # Update Appdata
    local date=$(date +%Y-%m-%d)

    if [ "$replace" -eq 0 ]; then
        sed -i "1,/^    <release .*$/ {/^    <release .*$/i\
        \ \ \ \ <release date=\"$date\" version=\"$(current_version)\" />
        }" "${SCRIPT_PATH}/../resources-templates/com.github.xournalpp.xournalpp.appdata.xml.in"
    else
        sed -i "s/\ \ \ \ <release date=\".*\" version=\"${prior_version}\" \/>/\ \ \ \ <release date=\"$date\" version=\"$(current_version)\" \/>/g" "${SCRIPT_PATH}/../resources-templates/com.github.xournalpp.xournalpp.appdata.xml.in"
    fi

    # Update MacOS Info.plist
    sed -i "s/<string>${prior_version}<\/string>/<string>$(current_version)<\/string>/g" "${SCRIPT_PATH}/../mac-setup/Info.plist"
    sed -i "s/<string>${prior_version}.0<\/string>/<string>$(current_version).0<\/string>/g" "${SCRIPT_PATH}/../mac-setup/Info.plist"

    # Update Fedora xournalpp.spec
    sed -i "s/%global	version_string ${prior_version}/%global	version_string $(current_version)/g" "${SCRIPT_PATH}/../rpm/fedora/xournalpp.spec"
}

# Prepares a new version
# A new version differs in the major or minor version part from the last.
# Assumes it is on the main development branch
# $1 The version string for the new version
function prepare_new_version() {
    if [[ $(parse_patch_version "$1") -ne 0 ]]; then
        abort 6 "The patch level must be '0'."
    fi

    # Check for a valid modification of the version strings
    if ! compare_versions "$(current_version)" "$1"; then
        abort 7 "The provided version is not higher than the current one."
        print_version_help
    fi

    # Create release branch
    local branch_name="release-$(parse_major_version "$1").$(parse_minor_version "$1")"

    # Checkout release branch
    if ! git checkout --quiet -b "$branch_name"; then
        abort 11 "Could not check out new release branch"
    fi

    # Bump version of release branch
    local new_version="$(echo "$1" | sed 's/+dev$//g')~dev"
    
    if ! bump_version "$new_version" 1; then
        abort 9 "Could not bump version of release branch"
    fi

    # Commit version bump
    if ! git commit --quiet -a -m "Automated version bump to $new_version"; then
        abort 10 "Could not commit version bump on release branch"
    fi

    echo "SUCCESS: New release $new_version was successfully prepared"
    echo "You are now on the release branch ($branch_name)."
    echo "Please check the last commit on the current branch and master for consistency"
    echo "and push your changes with:"
    echo ""
    echo "    git push origin master $branch_name"
    echo ""
    echo "Should the commit not meet your expectations, you may amend the changes."
    echo "BUT do not modify the version numbers!"
}

# Prepares a hotfix
# A hotfix may only differ in the version suffix from its base release
# Assumes it is on the commit of the release
# $1 The version string for the hotfix
function prepare_hotfix() {
    # Check for a valid modification of the version strings
    if [[ $(parse_major_version "$1") -ne $(current_major_version) ]]; then
        abort 6 "The major version may not differ from the base release for a hotfix."
    fi
    if [[ $(parse_minor_version "$1") -ne $(current_minor_version) ]]; then
        abort 6 "The minor version may not differ from the base release for a hotfix."
    fi
    if [[ $(parse_patch_version "$1") -ne $(current_patch_version) ]]; then
        abort 6 "The patch version may not differ from the base release for a hotfix."
    fi

    if ! compare_versions "$(current_version)" "$1"; then
        abort 7 "The provided version is not higher than the release it is based on."
        print_version_help
    fi

    local branch_name="hotfix-$1"

    # Check if branch already exists
    if branch_exists "$branch_name"; then
        abort 8 "The branch for this release already exists. Use this branch directly instead."
    fi

    # Check if hotfix was already released
    if release_exists "$1"; then
        abort 8 "The release already exists."
    fi

    # Check out new release-branch
    if ! git checkout --quiet -b "$branch_name"; then
        abort 9 "Could not check out new release branch"
    fi

    # Bump version
    if ! bump_version "$1~dev" 0; then
        abort 10 "Could not set version of release"
    fi

    # Commit version bump
    if ! git commit --quiet -a -m "Automated version bump to $1"; then
        abort 10 "Could not commit version bump on new release branch"
    fi

    echo "SUCCESS: New release $1 was successfully prepared"
    echo "You are now on the release branch ($branch_name)."
    echo "Please check the last commit on the current branch for consistency and push your changes with:"
    echo ""
    echo "    git push origin"
    echo ""
}

function publish_release() {
    # Determine on which branch we are
    local branch=
    if ! branch=$(git branch --show-current); then
        abort 2 "Could not determine the current branch"
    fi

    if ! [[ $branch =~ ^(release-[0-9]+\.[0-9]+|hotfix-[0-9]+\.[0-9]+\.[0-9]+([+~][A-Za-z0-9+~\.-]*)*)?$ ]]; then
        abort 4 "You are not on a release or hotfix branch. Are you on the right branch?"
    fi

    read -re -p "Are you sure you want to publish release $(current_version|sed 's/~dev$//g')? [y/N] "
    if ! [[ $REPLY =~ ^[Yy]+$ ]]; then
        exit 0
    fi

    #Strip ~dev from the version
    bump_version "$(echo "$(current_version)" | sed 's/~dev$//g')" 1

    # Commit version bump
    if ! git commit -a -m "Release $(current_version)"; then
        abort 5 "Could not commit version bump for release"
    fi

    # Get SHA of release commit
    sha=$(git rev-parse HEAD)

    # Store the version of the release
    release_version=$(current_version)

    # Tag the release
    echo "Tagging the release"
    if ! git tag -a "v$(current_version)" -m "Release $(current_version)"; then
        abort 6 "Could not tag release"
    fi

    # Bump the version to the next patch and add ~dev again if we are on a release-branch
    if [[ $branch =~ ^release-[0-9]+\.[0-9]+$ ]]; then
        bump_version "$(current_major_version).$(current_minor_version).$(($(current_patch_version)+1))$(current_suffix_version)~dev" 0

        # Commit version bump
        if ! git commit -a -m "Automated version bump to $(current_version)"; then
            abort 7 "Could not commit version bump after release"
        fi
    fi

    echo "SUCCESS: Release was published locally!"
    echo "To publish the release globally push your changes with:"
    echo ""
    echo "    git push --follow-tags origin ${branch}"
    echo ""

    read -re -p 'Do you want to merge the release back to the main development branch now? [Y/n] '
    if ! [[ $REPLY =~ ^[Nn]+$ ]]; then

        # Switch to main development branch
        if ! git checkout --quiet master; then
            abort 7 "Could not checkout main development branch"
        fi

        local old_version="$(current_version)"

        # Ensure that the git hooks are not placed already and that the sample for the pre-merge-commit hook is available
        if test -x "${SCRIPT_PATH}/../.git/hooks/commit-msg"; then
            abort 8 "Some git hooks already exist. This is not supported by this script."
        fi

        # If the current version of master is lower than the one we merge in, we must bump the version
        if compare_versions "$old_version" "$release_version"; then
            # Release version is higher
            echo ""
            echo "Be careful while patching! Read these instructions carefully before starting the merge:"
            echo "- Merge your version as if it will be the new version of the main development branch."
            echo "- There should not exist a development version anymore!"
            echo "  A new development version will be created automatically!"

            read -re -p "Start the merging process? [y/N] "
            if ! [[ $REPLY =~ ^[Yy]+$ ]]; then
                if ! git checkout --quiet "$branch"; then
                    abort 7 "Could not checkout release branch. BEWARE you are on 'master' now!"
                fi
                exit 0
            fi

            echo ""

            # These git hooks will be called before the merge commit is created. They will take care of bumping the version to the correct value.
            # Create the commit-msg hook
            cat << EOF > "${SCRIPT_PATH}/../.git/hooks/commit-msg"
#!/usr/bin/env bash

# Acquire helper methods
SOURCE_ONLY=1
source scripts/release_helper.sh

# Fix the script path as Git sets the working directory to the repository root
SCRIPT_PATH="scripts"

# Make sure the version is correct
if ! [[ "\$(current_version)" == "$release_version" ]]; then
    echo "The version information in CMakeLists.txt is not correct. The current version should be $release_version"
    exit 1
fi

# Make sure there is no artifact of an old development version
if ! [ -z "\$(cat CHANGELOG.md | grep -E "\+dev|~dev")" ]; then
    echo "CHANGELOG.md should not contain a development version string '[+~]dev'"
    exit 1
fi

if ! [ -z "\$(cat debian/changelog | grep -E "\+dev|~dev")" ]; then
    echo "debian/changelog should not contain a development version string '[+~]dev'"
    exit 1
fi

if ! [ -z "\$(cat desktop/com.github.xournalpp.xournalpp.appdata.xml | grep -E "\+dev|~dev")" ]; then
    echo "desktop/com.github.xournalpp.xournalpp.appdata.xml should not contain a development version string '[+~]dev'"
    exit 1
fi

# Bump the version to the next dev version
bump_version "${release_version}+dev" 0

# Amend the previous commit to include the version change
git add CMakeLists.txt CHANGELOG.md debian/changelog desktop/com.github.xournalpp.xournalpp.appdata.xml rpm/fedora/xournalpp.spec mac-setup/Info.plist

# Wait for merge to finish and then amend the commit (This hack is needed since git does not allow amendments in hooks)
bash -c "git merge HEAD &> /dev/null; while [ $? -ne 0 ]; do sleep 1; git merge HEAD &> /dev/null; done; git commit --amend -C HEAD --no-verify" &

# Remove the git hooks
rm .git/hooks/commit-msg

# Signify git that it can proceed with the merge
exit 0
EOF

            # The post-commit hook must be executable
            chmod +x "${SCRIPT_PATH}/../.git/hooks/commit-msg"

            # Start merge
            git merge --no-ff -m "Merge back Release ${release_version}" "$sha"

        else
            # Release version is lower
            echo ""
            echo "Be careful while patching! Read these instructions carefully before starting the merge:"
            echo "- Merge your version as if it is a historic release and do not modify or delete other versions"
            echo "- The versions in the changelog should be ordered in a descending order (highest first)"
            echo "- There should not exist any development version lower than your published release"

            read -re -p "Start the merging process? [y/N] "
            if ! [[ $REPLY =~ ^[Yy]+$ ]]; then
                if ! git checkout --quiet "$branch"; then
                    abort 7 "Could not checkout release branch. BEWARE you are on 'master' now!"
                fi
                exit 0
            fi

            echo ""

            # These git hooks will be called before the merge commit is created. They will take care of bumping the version to the correct value.
            # Enable the pre-merge-commit hook as it will call the pre-commit hook
            mv "${SCRIPT_PATH}/../.git/hooks/pre-merge-commit.sample" "${SCRIPT_PATH}/../.git/hooks/pre-merge-commit"
            # Create the pre-commit hook
            cat << EOF > "${SCRIPT_PATH}/../.git/hooks/pre-commit"
#!/usr/bin/env bash

# Acquire helper methods
SOURCE_ONLY=1
source scripts/release_helper.sh

# Fix the script path as Git sets the working directory to the repository root
SCRIPT_PATH="scripts"

# Make sure the version is correct
if ! [[ "\$(current_version)" == "$old_version" ]]; then
    echo "The version information in CMakeLists.txt is not correct. The current version should be $old_version"
    exit 1
fi

# Make sure the latest version did not change in the changelogs
if ! [[ "\$(sed -n "N;N;s/# Changelog\n\n## \([^\ ]*\) (Unreleased)/\1/p" CHANGELOG.md)" == "$old_version" ]]; then
    echo "The first version in CHANGELOG.md should be $old_version"
    exit 1
fi

if ! [[ "\$(sed -n "1,/xournalpp/{s/xournalpp (\([^-]*-1\)) UNRELEASED; urgency=medium/\1/p}" debian/changelog)" == "$old_version-1" ]]; then
    echo "The first version in debian/changelog should be $old_version"
    exit 1
fi

if ! [[ "\$(sed -n "1,/\ \ \ \ <release /{s/\ \ \ \ <release date=\"[^\"]*\" version=\"\([^\"]*\)\" \/>/\1/p}" desktop/com.github.xournalpp.xournalpp.appdata.xml)" == "$old_version" ]]; then
    echo "The first version in desktop/com.github.xournalpp.xournalpp.appdata.xml should be $old_version"
    exit 1
fi

# Remove the git hooks
mv .git/hooks/pre-merge-commit .git/hooks/pre-merge-commit.sample
rm .git/hooks/pre-commit

# Signify git that it can proceed with the merge
exit 0
EOF

            # The pre-commit hook must be executable
            chmod +x "${SCRIPT_PATH}/../.git/hooks/pre-commit"

            # Start merge
            git merge --no-ff -m "Merge back Release ${release_version}" "$sha"

        fi
    fi
}

if [ -z ${SOURCE_ONLY+x} ]; then
    ####################
    # Main functionality
    ####################

    # Check for a version number passed by argument
    if [ "$#" -lt 1 ]; then
        echo "Missing command"
        command="help"
    else
        command=$1
    fi

    if [[ $command != @(help|prepare|publish) ]]; then
        echo "ABORT: unknown command"
        command="help"
    fi

    if [[ $command == "help" ]]; then
        echo "Usage: $0 <command> <arguments>"
        echo ""
        echo "Commands:"
        echo "    prepare <version>  Prepares a release in the form of a new branch with correct version strings."
        echo "                     This command may only be run on a clean HEAD from:"
        echo "                       - The main development branch (master)"
        echo "                           The supplied version must differ from the current version in the major"
        echo "                           or minor version level. The suffix string may differ in any way."
        echo "                       - A commit of a published release (has a tag starting with 'v')"
        echo "                           The supplied version must differ from the current version in the suffix"
        echo "                           and must be higher than the current version."
        echo "                     The supplied version suffix may not end with '+dev'. This ending is a"
        echo "                     protected suffix and is applied by the script automatically."
        echo ""
        echo "    publish          Publishes a priorly prepared release. You must be on a branch that that was"
        echo "                     created during the prepare phase of this script. The version of the release"
        echo "                     is derived from the prepare phase. Make sure to update the changelogs prior"
        echo "                     to starting this phase. Changelogs are contained in:"
        echo "                       - CHANGELOG"
        echo "                       - debian/changelog"
        echo ""
        echo "    help             Prints this help message"
        echo ""
    fi

    # Check on which branch we are
    if ! branch=$(git branch --show-current); then
        abort 2 "Could not determine the current branch"
    fi

    # Check for a clean git working space - otherwise this script will commit whatever is there
    if ! git diff --quiet --cached --exit-code > /dev/null; then
        abort 3 "Your working tree is not clean. Please commit or stash all staged changes before running this script."
    fi
    if ! git diff --quiet --exit-code > /dev/null; then
        abort 3 "Your working tree is not clean. Please commit or stash all staged changes before running this script."
    fi

    if [[ $command == "prepare" ]]; then
        version=$2

        if validate_version "$version"; then
            print_version_help
            abort 4 "No valid version string provided"
        fi

        if [[ $version =~ [~+]dev$ ]]; then
            abort 5 "You may not use a suffix ending with '+dev' or '~dev'"
        fi

        if [[ $branch == "master" ]]; then
            # Disallow detached HEAD
            if is_detached; then
                abort 6 "You can not prepare a release in detached HEAD mode"
            fi
            prepare_new_version "$version"
        elif [[ $(git tag --contains | grep -Exc '^v[0-9]+.[0-9]+.[0-9]+([+~][0-9A-Za-z+~.-]*)*$') -ne 0 ]]; then
            prepare_hotfix "$version"
        else
            abort 4 "You may only call this script from the main development branch, an existing release branch or a tagged release."
        fi
    elif [[ $command == "publish" ]]; then
        publish_release
    fi
fi