Skip to content

Commit

Permalink
feat: add in better version formatting system
Browse files Browse the repository at this point in the history
  • Loading branch information
idc101 committed Apr 2, 2020
1 parent d1d0228 commit 0972e25
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 108 deletions.
1 change: 1 addition & 0 deletions docs/_includes/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ <h2><a href="installation.html">Installation</a></h2>
<h2><a href="usage.html">Usage</a></h2>
<h2><a href="common_patterns.html">Common Patterns</a></h2>
<h2><a href="config_reference.html">Config Reference</a></h2>
<h2><a href="formats.html">Version Formatting</a></h2>
</nav>
</div>
25 changes: 25 additions & 0 deletions docs/formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Format System

git-mkver includes a powerful string formatting system for creating version strings in different styles on different
branches. This is required as different software often have different restrictions on what a valid version number might be.

For example git is happy with the SemVer standard for tagging but docker does not support the `+` symbol in docker tags.

All format strings start with a `%`. They are recursively replaced so that one may refer to another.

## Built-in Formats

| Format Token | Substitution |
| ------------- | ------------- |
| `x` | Version major number |
| `z` | Version patch number |
| `y` | Version minor number |
| `br` | Branch name |
| `sh` | Short Hash |
| `hash` | Full Hash |
| `dd` | Day |
| `mm` | MonthValue, |
| `yyyy` | Year, |
| `bn` | Build No from build system |
| `tag` | Full tag |
| `pr` | Tag prefix |
52 changes: 24 additions & 28 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defaults {
prefix: v
tagMessageFormat: "release %tag"
tagParts: VersionBuildMetadata
tagFormat: version-buildmetadata
#minimumVersionIncrement: Major|Minor|Patch|PreRelease|None
patches: [
helm-chart
Expand All @@ -12,48 +12,44 @@ branches: [
{
name: "master"
tag: true
tagParts: Version
tagFormat: version
}
{
name: ".*"
tag: false
// formats: [
// {
// name: docker
// format: "%docker-branch"
// }
// ]
formats: [
{
name: docker
format: "%docker-branch"
}
]
}
]
patches: [
{
name: helm-chart
filePatterns: ["**/Chart.yaml"]
find: "version: .*"
replace: "version: \"%ver\""
replace: "version: \"%version\""
}
{
name: csproj
filePatterns: ["**/*.csproj"]
find: "<Version>.*</Version>"
replace: "<Version>%ver</Version>"
replace: "<Version>%version</Version>"
}
]
//formats: [
// {
// name: docker
// format: "%ver"
// }
// {
// name: docker-branch
// format: "%ver.%br.%sha"
// }
// {
// name: docker
// format: "%ver"
// }
// {
// name: docker-branch
// format: "%ver.%br.%sha"
// }
//]
formats: [
{
name: buildmetadata
format: "%br.%sh"
}
{
name: docker
format: "%version"
}
{
name: docker-branch
format: "%version.%br.%sha"
}
]
77 changes: 39 additions & 38 deletions src/main/scala/net/cardnell/mkver/AppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,77 +10,65 @@ import better.files.File
import com.typesafe.config.ConfigFactory
import zio.config.typesafe.{TypeSafeConfigSource, TypesafeConfig}

case class Format(name: String, format: String)
object Format {
val formatDesc = (
string("name").describe("Name of format. Do not prefix with %. e.g. 'major-minor'") |@|
string("format").describe("Format string for this format. Can include other formats. e.g. '%x.%y'")
)(Format.apply, Format.unapply)
}

case class BranchConfig(name: String,
prefix: String,
tag: Boolean,
tagParts: TagParts,
tagFormat: String,
tagMessageFormat: String,
preReleaseName: String,
buildMetadataFormat: String,
formats: List[Format],
patches: List[String])

case class BranchConfigOpt(name: String,
prefix: Option[String],
tag: Option[Boolean],
tagParts: Option[TagParts],
tagMessageFormat: Option[String],
preReleaseName: Option[String],
buildMetadataFormat: Option[String],
patches: Option[List[String]])
prefix: Option[String],
tag: Option[Boolean],
tagFormat: Option[String],
tagMessageFormat: Option[String],
preReleaseName: Option[String],
formats: Option[List[Format]],
patches: Option[List[String]])

object BranchConfig {
val nameDesc = string("name").describe("regex to match branch name on")
val prefixDesc = string("prefix").describe("prefix for git tags")
val tagDesc = boolean("tag").describe("whether to actually tag this branch when `mkver tag` is called")
val tagPartsDesc = string("tagParts")(TagParts.apply, TagParts.unapply).describe("")
val tagFormatDesc = string("tagFormat").describe("")
val tagMessageFormatDesc = string("tagMessageFormat").describe("")
val preReleaseNameDesc = string("preReleaseName").describe("")
val buildMetadataFormatDesc = string("buildMetadataFormat").describe("format string to produce build metadata part of a semantic version")
val formatsDesc = nested("formats")(list(Format.formatDesc)).describe("custom format strings")
val patchesDesc = list(string("patches")).describe("Patch configs to be applied")

val branchConfigDesc = (
nameDesc.default(".*") |@|
prefixDesc.default("v") |@|
tagDesc.default(false) |@|
tagPartsDesc.default(TagParts.VersionBuildMetadata) |@|
tagFormatDesc.default("version") |@|
tagMessageFormatDesc.default("release %ver") |@|
preReleaseNameDesc.default("rc.") |@|
buildMetadataFormatDesc.default("%br.%sh") |@|
formatsDesc.default(Nil) |@|
patchesDesc.default(Nil)
)(BranchConfig.apply, BranchConfig.unapply)

val branchConfigOptDesc = (
nameDesc |@|
prefixDesc.optional |@|
tagDesc.optional |@|
tagPartsDesc.optional |@|
tagFormatDesc.optional |@|
tagMessageFormatDesc.optional |@|
preReleaseNameDesc.optional |@|
buildMetadataFormatDesc.optional |@|
formatsDesc.optional |@|
patchesDesc.optional
)(BranchConfigOpt.apply, BranchConfigOpt.unapply)
}

sealed trait TagParts
object TagParts {
case object Version extends TagParts
//case object VersionPreRelease extends TagParts
case object VersionBuildMetadata extends TagParts
//case object VersionPreReleaseBuildMetadata extends TagParts

def apply(tagParts: String): TagParts = {
tagParts match {
case "Version" => Version
//case "VersionPreRelease" => VersionPreRelease
case "VersionBuildMetadata" => VersionBuildMetadata
//case "VersionPreReleaseBuildMetadata" => VersionPreReleaseBuildMetadata
}
}

def unapply(arg: TagParts): Option[String] = Some(arg.toString)
}

case class PatchConfig(name: String, filePatterns: List[String], find: String, replace: String)

object PatchConfig {
Expand All @@ -92,14 +80,14 @@ object PatchConfig {
)(PatchConfig.apply, PatchConfig.unapply)
}

case class AppConfig(defaults: BranchConfig, branches: List[BranchConfigOpt], patches: List[PatchConfig], formats: List[String])
case class AppConfig(defaults: BranchConfig, branches: List[BranchConfigOpt], patches: List[PatchConfig], formats: List[Format])

object AppConfig {
val appConfigDesc = (
nested("defaults")(BranchConfig.branchConfigDesc) |@|
nested("branches")(list(BranchConfig.branchConfigOptDesc)) |@|
nested("patches")(list(PatchConfig.patchConfigDesc)) |@|
list(string("formats")).default(Nil)
nested("formats")(list(Format.formatDesc)).default(Nil)
)(AppConfig.apply, AppConfig.unapply)

def getBranchConfig(currentBranch: String): BranchConfig = {
Expand All @@ -113,15 +101,28 @@ object AppConfig {
name = bc.name,
prefix = bc.prefix.getOrElse(defaults.prefix),
tag = bc.tag.getOrElse(defaults.tag),
tagParts = bc.tagParts.getOrElse(defaults.tagParts),
tagFormat = bc.tagFormat.getOrElse(defaults.tagFormat),
tagMessageFormat = bc.tagMessageFormat.getOrElse(defaults.tagMessageFormat),
preReleaseName = bc.preReleaseName.getOrElse(defaults.preReleaseName),
buildMetadataFormat = bc.buildMetadataFormat.getOrElse(defaults.buildMetadataFormat),
formats = mergeFormats(bc.formats.getOrElse(Nil), defaults.formats, appConfig.formats),
patches = bc.patches.getOrElse(defaults.patches)
)
}.getOrElse(defaults)
}

def mergeFormats(branch: List[Format], defaults: List[Format], appConfigFormats: List[Format]): List[Format] = {
def update(startList: List[Format], overrides: List[Format]): List[Format] = {
val startMap = startList.map( it => (it.name, it)).toMap
val overridesMap = overrides.map( it => (it.name, it)).toMap
overridesMap.values.foldLeft(startMap)((a, n) => a.+((n.name, n))).values.toList
}
// Start with defaults
val v1 = update(appConfigFormats, defaults)
val v2 = update(v1, branch)
val v3 = update(v2, Formatter.builtInFormats)
v3
}

def getPatchConfigs(branchConfig: BranchConfig): List[PatchConfig] = {
val allPatchConfigs = getAppConfig().patches.map(it => (it.name, it)).toMap
branchConfig.patches.map(allPatchConfigs.get(_).orElse(sys.error("Can't find patch config")).get)
Expand Down
47 changes: 47 additions & 0 deletions src/main/scala/net/cardnell/mkver/Formatter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.cardnell.mkver

object Formatter {
val builtInFormats = List(
Format("version", "%x.%y.%z"),
Format("version-prerelease", "%version-%prerelease"),
Format("version-buildmetadata", "%version+%buildmetadata"),
Format("version-prerelease-buildmetadata", "%version-%prerelease+%buildmetadata"),
)

val defaultFormats = List(
Format("prerelease", "rc-%prereleasename"),
Format("buildmetadata", "%br.%sh"),
)

case class Formatter(formats: List[Format]) {
def format(input: String): String = {
val result = formats.sortBy(_.name.length * -1).foldLeft(input) { (s, v) =>
s.replace("%" + v.name, v.format)
}
if (result == input) {
// no replacements made - we are done
result
} else {
// recursively replace
format(result)
}
}
}

def apply(version: VersionData, branchConfig: BranchConfig): Formatter = {
Formatter(List(
Format("x", version.major.toString),
Format("y", version.minor.toString),
Format("z", version.patch.toString),
Format("br", version.branch.replace("/", "-")),
Format("sh", version.commitHashShort),
Format("hash", version.commitHashFull),
Format("dd", version.date.getDayOfMonth.formatted("00")),
Format("mm", version.date.getMonthValue.formatted("00")),
Format("yyyy", version.date.getYear.toString),
Format("bn", version.buildNo),
Format("tag?", branchConfig.tag.toString),
Format("pr", branchConfig.prefix.toString)
) ++ branchConfig.formats)
}
}
6 changes: 3 additions & 3 deletions src/main/scala/net/cardnell/mkver/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Main(git: Git.Service = Git.Live.git()) {
val config = AppConfig.getBranchConfig(currentBranch)
val nextVersionData = getNextVersion(git, config, currentBranch)
val output = nextOpts.format.map { format =>
VariableReplacer(nextVersionData, config).replace(format)
Formatter(nextVersionData, config).format(format)
}.getOrElse(formatTag(config, nextVersionData))
println(output)
}
Expand All @@ -48,7 +48,7 @@ class Main(git: Git.Service = Git.Live.git()) {
val config = AppConfig.getBranchConfig(currentBranch)
val nextVersion = getNextVersion(git, config, currentBranch)
val tag = formatTag(config, nextVersion)
val tagMessage = VariableReplacer(nextVersion, config).replace(config.tagMessageFormat)
val tagMessage = Formatter(nextVersion, config).format(config.tagMessageFormat)
if (config.tag && nextVersion.commitCount > 0) {
git.tag(tag, tagMessage)
}
Expand All @@ -60,7 +60,7 @@ class Main(git: Git.Service = Git.Live.git()) {
val nextVersion = getNextVersion(git, config, currentBranch)
AppConfig.getPatchConfigs(config).foreach { patch =>
val regex = patch.find.r
val replacement = VariableReplacer(nextVersion, config).replace(patch.replace)
val replacement = Formatter(nextVersion, config).format(patch.replace)
patch.filePatterns.foreach { filePattern =>
File.currentWorkingDirectory.glob(filePattern, includePath = false).foreach { file =>
println(s"patching: $file, replacement: $replacement")
Expand Down
13 changes: 5 additions & 8 deletions src/main/scala/net/cardnell/mkver/MkVer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,12 @@ object MkVer {
}

def formatTag(config: BranchConfig, versionData: VersionData): String = {
val version = s"${config.prefix}${versionData.major}.${versionData.minor}.${versionData.patch}"
val preRelease = "TODO"
val buildMetaData = VariableReplacer(versionData, config).replace(config.buildMetadataFormat)
config.tagParts match {
case TagParts.Version => version
//case TagParts.VersionPreRelease => s"$version-$preRelease"
case TagParts.VersionBuildMetadata =>s"$version+$buildMetaData"
//case TagParts.VersionPreReleaseBuildMetadata =>s"$version-$preRelease+$buildMetaData"
val allowedFormats = Formatter.builtInFormats.map(_.name)
if (!allowedFormats.contains(config.tagFormat)) {
System.err.println(s"tagFormat (${config.tagFormat}) must be one of: ${allowedFormats.mkString(", ")}")
sys.exit(1)
}
Formatter(versionData, config).format(s"${config.prefix}%${config.tagFormat}")
}

def getNextVersion(git: Git.Service, config: BranchConfig, currentBranch: String): VersionData = {
Expand Down
31 changes: 0 additions & 31 deletions src/main/scala/net/cardnell/mkver/VariableReplacer.scala

This file was deleted.

Loading

0 comments on commit 0972e25

Please sign in to comment.