diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 57c58bb92..d081cb5a5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -167,8 +167,7 @@ release:prepare: - if: $CI_COMMIT_TAG =~ /^v[0-9\.]+-test-.*$/ script: - echo "Preparing release..." - # TODO: get tag and pass to following release jobs as artifact - - echo FOOBAR > .current-version + - ./.gitlab-ci/version.pl > .current-version artifacts: paths: - .current-version diff --git a/.gitlab-ci/version.pl b/.gitlab-ci/version.pl new file mode 100755 index 000000000..80eae5e9f --- /dev/null +++ b/.gitlab-ci/version.pl @@ -0,0 +1,483 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Data::Dumper; + +# Version changes: +# v[x].[y].[z] -- Main version number +# v[x].[y].[z]-test-[branchstring]-num -- test/branch/devel version number +# on main/master: Biggest version so far, increment by occuring changes +# on other branches: find version; be it branch string, old format or main version number; +# increments from there. Increment version number, but on global conflict use new version number + +# Actions and their results +# chore -> +patch +# feat -> +minor +# fix -> +patch +# [a-z]+! -> +major +# perf -> +patch +# refactor -> +patch +# test -> +patch +# style -> +patch +# revert -> = +# docs -> +patch +# build -> = +# ci -> = + +# parameters with default values +my %par = (); +my %parKinds = ( + vcslog=>{ + arity=>1, + def=>'git log --pretty=tformat:"%H :::: %d :::: %s"', + help=>'set command which outputs the log information to be used; reads from STDIN if value is set to "-"', + }, + vcstags=>{ + arity=>1, + def=>'git tag', + help=>'set command which outputs the used tags', + }, + vcsbranch=>{ + arity=>1, + def=>'git rev-parse --abbrev-ref HEAD', + help=>'set command to find out the current branch name', + }, + kind=>{ + arity=>1, + def=>'v', + help=>'set tag kind of version numbers; this option resets autokind to "". Implemented kinds: v: main version; t: test version', + auto=>sub { $par{autokind}='' }, + }, + autokind=>{ + arity=>1, + def=>'main=v,master=v,test=t,*=t', + help=>'determine the tag kind from branch name instead of fixed value; use the first fitting glob', + }, + change=>{ + arity=>1, + def=>'chore=patch,feat=minor,feature=minor,fix=patch,BREAK=major,perf=patch,refactor=patch,test=patch,style=patch,revert=null,docs=patch,build=null,ci=null', + help=>'how to react on which commit type; can be partially given. Actions are: "null", "major", "minor", "patch" or state "invalid" for removing this type', + }, + v=>{def=>0,arity=>0,help=>'verbose'}, + h=>{def=>0,arity=>0,help=>'help'}, +); + +for my $k(keys %parKinds) { + $par{$k} = $parKinds{$k}{def} +} + +#for my $p(@ARGV) { +# +#} +{ + my $i = 0; + while($i<@ARGV) { + if($ARGV[$i]=~m#^-(.*)#) { + my $key = $1; + if(not exists $parKinds{$key}) { + die "$0: Unknown parameter: -$key\n"; + } + my $pk = $parKinds{$key}; + die "$0: Too few parameters for '-$key'\n" if $i+$pk->{arity}>@ARGV; + my @par = @ARGV[$i+1..$i+$pk->{arity}]; + #warn "<< @par >>"; + $i++; + $i += $pk->{arity}; + if($pk->{arity}) { + $par{$key} = $par[0] + } else { + $par{$key}=1 + } + if(exists $pk->{auto}) { + $pk->{auto}->() + } + } else { + die "$0: Bad parameter: $ARGV[$i]\n" + } + } +} + +if($par{'h'}) { + print "Usage: $0 [flags and options]\n\nAvailable options:\n"; + for my $k(sort keys %parKinds) { + print " -$k\n $parKinds{$k}{help}\n"; + if($parKinds{$k}{arity}) { + print " Default value: $parKinds{$k}{def}\n"; + } else { + print " This is a flag and not an option\n"; + } + print "\n"; + } + exit 0 +} + +if($par{autokind}) { + my $branch = `$par{vcsbranch}`; + my @rules = split /,/, $par{autokind}; + RULES: { + for my $r(@rules) { + if($r!~m#(.*)=(.*)#) { + die "$0: Bad rule in autokind: $r\n"; + } + my ($glob, $kind) = ($1, $2); + if(globString($glob, $branch)) { + $par{'kind'} = $kind; + last RULES + } + } + warn "$0: No autokind rule matches; leaving the kind unchanged.\n" + } +} + + +if($par{'v'}) { + print "VERBOSE: Parameters\n"; + for my $k(sort keys %par) { + print " $k: $par{$k}\n" + } +} + +my %typeReact = (); +for my $as(split /,/, $par{change}) { + if($as=~m#(.*)=(.*)#) { + $typeReact{$1} = $2; + } else { + warn "$0: Unexpected change parameter: '$as'" + } +} + +#my @have = split /\n/, `$par{vcstags}`; +# +#my @keep = grep { $_ } map { m#^($par{kind})([0-9].*)# ? [$1,$2] : undef } @have; +# +#my @oldVersions = (); + +sub globString { + my ($glob, $string) = @_; + my @glob = map { m#\*# ? '*' : $_ } $glob=~m#(\?|\*+|[^\?\*]+)#g; + my %matchCache = (); + my $match = undef; + my $matchCore = sub { + my ($i, $j) = @_; + return 1 if $i==@glob and $j==length $string; + return 0 if $i>=@glob or $j>=length $string; + return $match->($i+1,$j+1) if '?' eq $glob[$i]; + if('*' eq $glob[$i]) { + for my $jj($j..length($string)) { + return 1 if $match->($i+1, $jj); + } + return 0; + } + return $match->($i+1, $j+length($glob[$i])) if + $glob[$i] eq substr($string, $j, length($glob[$i])); + return 0 + }; + $match = sub { + my ($i, $j) = @_; + my $ij = "$i $j"; + my $res = $matchCache{$ij}; + if(not defined $res) { + $res = $matchCore->($i, $j); + $matchCache{$ij} = $res; + } + $res + }; + $match->(0,0); +} + +sub parseVersion { + my $v = shift; + if(not defined $v) { + my $c = join " ", caller; + warn "$0: internal error (parseVersion called on undef at $c)\n"; + return undef + } + my ($pre,$ma,$mi,$p,$sp,$brn,$brv) = (); + if($v=~m#^([a-z]*)([0-9]+)$#) { + $pre = $1; + $ma = $2; + } elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)$#) { + $pre = $1; + $ma = $2; + $mi = $3 + } elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)\.([0-9]+)$#) { + $pre = $1; + $ma = $2; + $mi = $3; + $p = $4; + } elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)\.([0-9]+)-test-([a-z]+)-([0-9\.]+)$#) { + $pre = $1; + $ma = $2; + $mi = $3; + $p = $4; + $sp = $5; + $brn = $6; + $brv = $7; + } elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)\.([0-9]+)-(.*)$#) { + $pre = $1; + $ma = $2; + $mi = $3; + $p = $4; + $sp = $5; + } else { + warn "$0: unexpected old version number: $v\n" if $par{v}; + return undef + } + $pre = 'v' if '' eq $pre; + return { + prefix=>$pre, + major=>$ma, + minor=>$mi, + patch=>$p, + subpatch=>$sp, + branchname=>$brn, + branchversion=>$brv, + } +} + +#@oldVersions = sort { +# ($a->{major} // 0) <=> ($b->{major} // 0) || +# ($a->{minor} // 0) <=> ($b->{minor} // 0) || +# ($a->{patch} // 0) <=> ($b->{patch} // 0) || +# ($a->{subpatch} // '') <=> ($b->{subpatch} // '') +#} @oldVersions; + +sub vsCompare { + my ($vp, $wp) = @_; + my ($v, $w) = ($vp, $wp); + my ($verr, $werr) = (0,0); + unless(ref $v) { + eval { $v = parseVersion($v) }; + $verr = 1 if $@ or not defined $v; + } + unless(ref $w) { + eval { $w = parseVersion($w) }; + $werr = 1 if $@ or not defined $w; + } + if($verr and $werr) { + return $vp cmp $wp; + } + if($verr) { + return -1 + } + if($werr) { + return 1 + } + #for($v, $w) { + # $_ = parseVersion($_) unless ref $_; + #} + if('v' eq $v->{prefix} and 'v' eq $w->{prefix}) { + return( + ($v->{major} // 0) <=> ($w->{major} // 0) || + ($v->{minor} // 0) <=> ($w->{minor} // 0) || + ($v->{patch} // 0) <=> ($w->{patch} // 0) || + ($v->{branchname} // '') cmp ($w->{branchname} // '') || + ($v->{branchversion} // '') <=> ($w->{branchversion} // '') || + ($v->{subpatch} // '') cmp ($w->{subpatch} // '') + ) + } elsif('v' eq $v->{prefix} and 'v' ne $w->{prefix}) { + return 1; + } elsif('v' ne $v->{prefix} and 'v' eq $w->{prefix}) { + return -1; + } else { + return vsStringDebug($v) cmp vsStringDebug($w) + } +} + +sub vsStringDebug { + my $v = shift; + my $ret = + ("[" . ($v->{prefix} // 'undef') . "]") . + ($v->{major} // 'undef') . "." . + ($v->{minor} // 'undef') . "." . + ($v->{patch} // 'undef'); + $ret .= "-[$v->{subpatch}]" if defined $v->{subpatch}; + $ret .= "-test-" . ($v->{branchname} // 'undef') . "-" . ($v->{branchversion} // 'undef'); + return $ret +} + +sub vsString { + my $v = shift; + my $ret = + ($v->{major} // 0) . "." . + ($v->{minor} // 0) . "." . + ($v->{patch} // 0); + $ret .= "-$v->{subpatch}" if defined $v->{subpatch}; + return $ret +} + +sub vsJustVersion { + my $v = shift; + my $ret = + ($v->{major} // 0) . "." . + ($v->{minor} // 0) . "." . + ($v->{patch} // 0); + return $ret +} + +sub vsTestVersion { + my $v = shift; + my $ret = + 'v' . + ($v->{major} // 0) . "." . + ($v->{minor} // 0) . "." . + ($v->{patch} // 0) . "-test-" . + ($v->{branchname} // 'a') . + ($v->{branchversion} // '0.0.0'); + return $ret +} + +#print vsStringDebug($_), "\n" for @oldVersions; + +#print " << $_->[1] >>\n" for @keep; + +my @versionsOrig = (); +if('-' eq $par{vcslog}) { + @versionsOrig = ; + chomp for @versionsOrig +} else { + @versionsOrig = split /\n/, `$par{vcslog}`; +} +my @versions = (); +for my $v(@versionsOrig) { + if($v=~m#^(.*?\S)\s*::::\s*(.*?)\s*::::\s*(.*)#) { + push @versions, { + hash => $1, + meta => $2, + subject => $3 + } + } +} + +#print Data::Dumper::Dumper(\@versions); + +my @change = (); +my $tag = undef; + +my @versionPast = (); + +VERSION: for my $v(@versions) { + #if($v->{meta}=~m#tag\s*:\s*\Q$par{kind}\E(.*)\)#) { + # $tag=$1; + # last VERSION + #} + if($v->{meta}=~m#tag\s*:\s*([vtd]b?[0-9\.]+(?:-.*)?)\)#) { + $v->{version} = $1; + push @versionPast, $v->{version} + } + next if $v->{subject}=~m#^\s*(?:Merge (?:branch|remote)|Revert )#; + if($v->{subject}=~m#^\s*([a-z]+)\s*(!?)\s*#) { + my ($type, $break) = ($1, $2); + if(exists $typeReact{$type}) { + my $react = $typeReact{$type}; + next VERSION if 'null' eq $react; + my %h = %$v; + $h{react} = $react; + push @change, \%h + } else { + warn "$0: cannot react on commit message '$v->{subject}', type '$type' unknown\n" if $par{$v}; + } + } else { + warn "$0: commit message not parseable: $v->{subject}\n" if $par{$v}; + } +} + +#$tag = parseVersion($tag); + +for my $r(reverse @change) { + if('major' eq $r->{react}) { + $tag->{major}++; + $tag->{minor}=0; + $tag->{patch}=0; + $tag->{subpatch}=undef; + } elsif('minor' eq $r->{react}) { + $tag->{minor}++; + $tag->{patch}=0; + $tag->{subpatch}=undef; + } elsif('patch' eq $r->{react}) { + $tag->{patch}++; + $tag->{subpatch}=undef; + } else { + die "$0: Cannot perform modification '$r->{react}' (probably internal error)" + } +} + +#print Data::Dumper::Dumper(\@change, $tag); +#for my $c(@change) { +# print "==\n"; +# for my $k(sort keys %$c) { +# print " $k: $c->{$k}\n" +# } +# print "\n" +#} +# +#print "\n"; +#for my $v(@versionPast) { +# my $vv = vsStringDebug(parseVersion($v)); +# print "VERSION $v --> $vv\n" +#} + +my @allVersions = split /\n/, `$par{vcstags}`; + +my @sortAll = sort {vsCompare($b, $a)} @allVersions; +my @sortSee = sort {vsCompare($b, $a)} @versionPast; +#print "all: $sortAll[0] -- see: $sortSee[0]\n"; +# +#print vsString($tag), "\n"; + +my $mainVersion = 'v' eq $par{kind}; + +my $highStart = $mainVersion ? $sortAll[0] : $sortSee[0]; +my $highSee = $sortSee[0]; +my %reactCollect = (); +SEARCHVERSION: for my $v(@versions) { + next unless $v->{version}; + next unless $v->{react}; + $reactCollect{$v->{react}} = 1; + if($highSee eq $v->{version}) { + last SEARCHVERSION; + } +} + +sub justVersionInc { + my ($v, $react) = @_; + my $vv = parseVersion($v); + $vv->{patch}++ if $react->{patch}; + do {$vv->{minor}++; $vv->{patch}=0} if $react->{minor}; + do {$vv->{major}++; $vv->{minor}=0; $vv->{patch}=0} if $react->{major}; + return vsJustVersion($vv); +} + +my $newVersion = undef; + +if($mainVersion) { + $newVersion = "v" . justVersionInc($highStart, \%reactCollect); +} else { + my $v = parseVersion($highStart); + if(exists $v->{branchname}) { + $v->{branchversion} = justVersionInc($v->{branchversion} // '0.0.0', \%reactCollect); + } else { + $v->{branchname} = 'a'; + $v->{branchversion} = '0.0.0'; + } + $newVersion = vsTestVersion($v); +} + +my %allVersions = (); +for(@allVersions) { + $allVersions{$_} = 1 +} +while(exists $allVersions{$newVersion}) { + if($mainVersion) { + die "$0: probably internal error (collision in main version)\n" + } + my $v = parseVersion($newVersion); + $v->{branchname} //= 'a'; + $v->{branchname}++; + $newVersion = vsTestVersion($v); +} + +print "$newVersion\n"; +