(#2043) release task: support SNAPSHOTs on qualifiers

Prior to this commit, the `lein release` task was not
really usable for releases that had qualifiers such
as `-alpha1`, `-beta2`, or `-RC1`.

The reason for this is that the code for parsing the
semver could not handle a qualifier like `-alpha1`
AND a `-SNAPSHOT` qualifier at the same time.

The default `:release-tasks` include a `bump-version`
at the `:release` level, and then the release, and
then a `bump-version` at the `:patch` level.  If
you had a version that started out as `1.0.0-alpha1` *or*
`1.0.0-alpha1-SNAPSHOT`, then the `:release` bump
would always set the version to `1.0.0`.  Then the
actual release would occur, and then the `:patch` bump
would take you to `1.0.1-SNAPSHOT`.

With this commit, `qualifier` and `snapshot` are
separated into two separate fields in the version map.
This allows us to modify the `bump-version` behavior
so that it works as one would expect with a qualifier
like `alpha`:

```clj
(is (= (bump-version "1.0.0-alpha1-SNAPSHOT" :release)
       "1.0.0-alpha1"))
(is (= (bump-version "1.0.0-alpha1")
       "1.0.0-alpha2-SNAPSHOT"))
```

This allows the default `:release-tasks` setup to be
used with qualifiers like alpha/beta.

The commit also adds a new `level` that can be passed
to the `change` task: `:qualifier`.  If this level
is passed, then the qualifier will be incremented
instead of the major/minor/patch.  It is a superset
of the existing alpha/beta/RC behavior but works with
arbitrary qualifier strings.
This commit is contained in:
Chris Price 2015-12-24 07:55:03 -08:00
parent 054a77af6c
commit ef4e178f81
2 changed files with 173 additions and 50 deletions

View file

@ -4,20 +4,22 @@
[leiningen.core.main :as main]
[leiningen.core.project :as project]))
(def ^:dynamic *level* :patch)
(def ^:dynamic *level* nil)
(defn string->semantic-version [version-string]
"Create map representing the given version string. Returns nil if the
string does not follow guidelines setforth by Semantic Versioning 2.0.0,
http://semver.org/"
;; <MajorVersion>.<MinorVersion>.<PatchVersion>[-<BuildNumber | Qualifier >]
(let [version-map (->> (re-matches #"(\d+)\.(\d+)\.(\d+).*" version-string)
(drop 1)
(map #(Integer/parseInt %))
(zipmap [:major :minor :patch]))
qualifier (last (re-matches #".*-(.+)?" version-string))]
(if-not (empty? version-map)
(merge version-map {:qualifier qualifier}))))
;; <MajorVersion>.<MinorVersion>.<PatchVersion>[-<Qualifier>][-SNAPSHOT]
(if-let [[_ major minor patch qualifier snapshot]
(re-matches
#"(\d+)\.(\d+)\.(\d+)(?:-(?!SNAPSHOT)([^\-]+))?(?:-(SNAPSHOT))?"
version-string)]
(->> [major minor patch]
(map #(Integer/parseInt %))
(zipmap [:major :minor :patch])
(merge {:qualifier qualifier
:snapshot snapshot}))))
(defn parse-semantic-version [version-string]
"Create map representing the given version string. Aborts with exit code 1
@ -29,32 +31,56 @@
(defn version-map->string
"Given a version-map, return a string representing the version."
[version-map]
(let [{:keys [major minor patch qualifier]} version-map]
(if qualifier
(str major "." minor "." patch "-" qualifier)
(str major "." minor "." patch))))
(let [{:keys [major minor patch qualifier snapshot]} version-map]
(cond-> (str major "." minor "." patch)
qualifier (str "-" qualifier)
snapshot (str "-" snapshot))))
(defn next-qualifier [sublevel qualifier]
(let [pattern (re-pattern (str sublevel "([0-9]+)"))
[_ n] (and qualifier (re-find pattern qualifier))]
(str sublevel (inc (Integer. (or n 0))))))
(defn next-qualifier
"Increments and returns the qualifier. If an explicit `sublevel`
is provided, then, if the original qualifier was using that sublevel,
increments it, else returns that sublevel with \"1\" appended.
Supports empty strings for sublevel, in which case the return value
is effectively a BuildNumber."
([qualifier]
(if-let [[_ sublevel] (re-matches #"([^\d]+)?(?:\d+)?"
(or qualifier ""))]
(next-qualifier sublevel qualifier)
"1"))
([sublevel qualifier]
(let [pattern (re-pattern (str sublevel "([0-9]+)"))
[_ n] (and qualifier (re-find pattern qualifier))]
(str sublevel (inc (Integer. (or n 0)))))))
(defn bump-version-map
"Given version as a map of the sort returned by parse-semantic-version, return
a map of the version incremented in the level argument. Add qualifier unless
releasing non-snapshot."
[{:keys [major minor patch qualifier]} level]
(case (keyword (name level))
:major {:major (inc major) :minor 0 :patch 0 :qualifier "SNAPSHOT"}
:minor {:major major :minor (inc minor) :patch 0 :qualifier "SNAPSHOT"}
:patch {:major major :minor minor :patch (inc patch) :qualifier "SNAPSHOT"}
:alpha {:major major :minor minor :patch patch
:qualifier (next-qualifier "alpha" qualifier)}
:beta {:major major :minor minor :patch patch
:qualifier (next-qualifier "beta" qualifier)}
:rc {:major major :minor minor :patch patch
:qualifier (next-qualifier "RC" qualifier)}
:release {:major major :minor minor :patch patch}))
a map of the version incremented in the level argument. Always returns a
SNAPSHOT version, unless the level is :release. For :release, removes SNAPSHOT
if the input is a SNAPSHOT, removes qualifier if the input is not a SNAPSHOT."
[{:keys [major minor patch qualifier snapshot]} level]
(let [level (or level
(if qualifier :qualifier)
:patch)]
(case (keyword (name level))
:major {:major (inc major) :minor 0 :patch 0 :qualifier nil :snapshot "SNAPSHOT"}
:minor {:major major :minor (inc minor) :patch 0 :qualifier nil :snapshot "SNAPSHOT"}
:patch {:major major :minor minor :patch (inc patch) :qualifier nil :snapshot "SNAPSHOT"}
:alpha {:major major :minor minor :patch patch
:qualifier (next-qualifier "alpha" qualifier)
:snapshot "SNAPSHOT"}
:beta {:major major :minor minor :patch patch
:qualifier (next-qualifier "beta" qualifier)
:snapshot "SNAPSHOT"}
:rc {:major major :minor minor :patch patch
:qualifier (next-qualifier "RC" qualifier)
:snapshot "SNAPSHOT"}
:qualifier {:major major :minor minor :patch patch
:qualifier (next-qualifier qualifier)
:snapshot "SNAPSHOT"}
:release (merge {:major major :minor minor :patch patch}
(if snapshot
{:qualifier qualifier :snapshot nil}
{:qualifier nil :snapshot nil})))))
(defn bump-version
"Given a version string, return the bumped version string -
@ -100,9 +126,9 @@ is a task name and the rest are arguments to that task.
The release task takes a single argument which should be one of :major,
:minor, :patch, :alpha, :beta, or :rc to indicate which version level to
bump. If none is given, it defaults to :patch."
([project] (release project (str *level*)))
([project] (release project *level*))
([project level]
(binding [*level* (read-string level)]
(binding [*level* (if level (read-string level))]
(doseq [task (:release-tasks project)]
(let [current-project (project/init-project (project/read))]
(main/resolve-and-apply current-project task))))))

View file

@ -12,55 +12,151 @@
{:major 1
:minor 0
:patch 0
:qualifier nil}
:qualifier nil
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.1.0-SNAPSHOT"
:patch "1.0.1-SNAPSHOT"}]
:patch "1.0.1-SNAPSHOT"
:release "1.0.0"
:alpha "1.0.0-alpha1-SNAPSHOT"
:beta "1.0.0-beta1-SNAPSHOT"
:rc "1.0.0-RC1-SNAPSHOT"
:qualifier "1.0.0-1-SNAPSHOT"}]
["1.2.3"
{:major 1
:minor 2
:patch 3
:qualifier nil}
:qualifier nil
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.3.0-SNAPSHOT"
:patch "1.2.4-SNAPSHOT"}]
:patch "1.2.4-SNAPSHOT"
:release "1.2.3"
:alpha "1.2.3-alpha1-SNAPSHOT"
:beta "1.2.3-beta1-SNAPSHOT"
:rc "1.2.3-RC1-SNAPSHOT"
:qualifier "1.2.3-1-SNAPSHOT"}]
["1.2.3-herp"
{:major 1
:minor 2
:patch 3
:qualifier "herp"}
:qualifier "herp"
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.3.0-SNAPSHOT"
:patch "1.2.4-SNAPSHOT"
:release "1.2.3"}]
:release "1.2.3"
:alpha "1.2.3-alpha1-SNAPSHOT"
:beta "1.2.3-beta1-SNAPSHOT"
:rc "1.2.3-RC1-SNAPSHOT"
:qualifier "1.2.3-herp1-SNAPSHOT"}]
["1.0.0-SNAPSHOT"
{:major 1
:minor 0
:patch 0
:qualifier "SNAPSHOT"}
:qualifier nil
:snapshot "SNAPSHOT"}
{:major "2.0.0-SNAPSHOT"
:minor "1.1.0-SNAPSHOT"
:patch "1.0.1-SNAPSHOT"
:release "1.0.0"
:alpha "1.0.0-alpha1"
:beta "1.0.0-beta1"
:rc "1.0.0-RC1"}]
:alpha "1.0.0-alpha1-SNAPSHOT"
:beta "1.0.0-beta1-SNAPSHOT"
:rc "1.0.0-RC1-SNAPSHOT"
:qualifier "1.0.0-1-SNAPSHOT"}]
["1.0.0-alpha1"
{:major 1
:minor 0
:patch 0
:qualifier "alpha1"}
:qualifier "alpha1"
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.1.0-SNAPSHOT"
:patch "1.0.1-SNAPSHOT"
:release "1.0.0"
:alpha "1.0.0-alpha2"
:beta "1.0.0-beta1"
:rc "1.0.0-RC1"}]])
:alpha "1.0.0-alpha2-SNAPSHOT"
:beta "1.0.0-beta1-SNAPSHOT"
:rc "1.0.0-RC1-SNAPSHOT"
:qualifier "1.0.0-alpha2-SNAPSHOT"}]
["1.0.0-alpha1-SNAPSHOT"
{:major 1
:minor 0
:patch 0
:qualifier "alpha1"
:snapshot "SNAPSHOT"}
{:major "2.0.0-SNAPSHOT"
:minor "1.1.0-SNAPSHOT"
:patch "1.0.1-SNAPSHOT"
:release "1.0.0-alpha1"
:alpha "1.0.0-alpha2-SNAPSHOT"
:beta "1.0.0-beta1-SNAPSHOT"
:rc "1.0.0-RC1-SNAPSHOT"
:qualifier "1.0.0-alpha2-SNAPSHOT"}]
["1.0.0-beta1"
{:major 1
:minor 0
:patch 0
:qualifier "beta1"
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.1.0-SNAPSHOT"
:patch "1.0.1-SNAPSHOT"
:release "1.0.0"
:alpha "1.0.0-alpha1-SNAPSHOT"
:beta "1.0.0-beta2-SNAPSHOT"
:rc "1.0.0-RC1-SNAPSHOT"
:qualifier "1.0.0-beta2-SNAPSHOT"}]
["1.0.0-RC2-SNAPSHOT"
{:major 1
:minor 0
:patch 0
:qualifier "RC2"
:snapshot "SNAPSHOT"}
{:major "2.0.0-SNAPSHOT"
:minor "1.1.0-SNAPSHOT"
:patch "1.0.1-SNAPSHOT"
:release "1.0.0-RC2"
:alpha "1.0.0-alpha1-SNAPSHOT"
:beta "1.0.0-beta1-SNAPSHOT"
:rc "1.0.0-RC3-SNAPSHOT"
:qualifier "1.0.0-RC3-SNAPSHOT"}]
["1.2.3-herp2"
{:major 1
:minor 2
:patch 3
:qualifier "herp2"
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.3.0-SNAPSHOT"
:patch "1.2.4-SNAPSHOT"
:release "1.2.3"
:alpha "1.2.3-alpha1-SNAPSHOT"
:beta "1.2.3-beta1-SNAPSHOT"
:rc "1.2.3-RC1-SNAPSHOT"
:qualifier "1.2.3-herp3-SNAPSHOT"}]
["1.2.3-25"
{:major 1
:minor 2
:patch 3
:qualifier "25"
:snapshot nil}
{:major "2.0.0-SNAPSHOT"
:minor "1.3.0-SNAPSHOT"
:patch "1.2.4-SNAPSHOT"
:release "1.2.3"
:alpha "1.2.3-alpha1-SNAPSHOT"
:beta "1.2.3-beta1-SNAPSHOT"
:rc "1.2.3-RC1-SNAPSHOT"
:qualifier "1.2.3-26-SNAPSHOT"}]])
(deftest test-string->semantic-version
(testing "Testing semantic version string parsing"
@ -88,6 +184,7 @@
(deftest version-map->string-valid
(doseq [[string parsed bumps] valid-semver-version-values]
(is (= string (version-map->string parsed)))
(doseq [[level string] bumps]
(is (= (merge {:qualifier nil} (bump-version-map parsed level))
(parse-semantic-version string))))))
(doseq [[level expected-bumped-string] bumps]
(let [bumped (bump-version-map parsed level)]
(is (= bumped (parse-semantic-version expected-bumped-string)))
(is (= expected-bumped-string (version-map->string bumped)))))))