Shantanu Kumar avatar Shantanu Kumar committed 5d59e58

Integrate with Clj-Liquibase - command-line parsing, update, rollback, tag, dbdoc, integrate with Clj-DBCP

Comments (0)

Files changed (7)

 
 # Changes and TODO
 
-## TODO (Please contribute feedback, ideas, code, documentation, donation...)
+- [TODO] Other Clj-Liquibase commands: Reverse-engineer schema
 
-- [TODO] DSL for specifying change-set in Clojure and convert to XML at runtime
+
+## 0.2 / 2011-Apr-05
+
+- Pick up var that defines changelog (Clj-Liquibase) from application sources
+- Clj-Liquibase commands: Update, Rollback, Tag, DBDoc
+- Allow defaults in project.clj (key :lein-lb) of application
+- Integrate with Clj-DBCP for database connections
 
 
 ## 0.1 / 2010-Nov-?? (GMT + 5:30)
-(defproject org.bituf/lein-lb "0.1"
-  :description "Leiningen plugin for Liquibase: http://www.liquibase.org/"
+(defproject org.bituf/lein-lb "0.2-SNAPSHOT"
+  :description "Leiningen plugin for Clj-Liquibase: https://bitbucket.org/kumarshantanu/clj-liquibase/src"
   :url         "http://code.google.com/p/bitumenframework/"
-  :dependencies [[org.liquibase/liquibase-core "2.0-rc6"]]
-  :dev-dependencies [[org.clojure/clojure         "1.2.0"]
-                     [org.clojure/clojure-contrib "1.2.0"]])
+  :dependencies [[org.bituf/clj-liquibase "0.2"] ; pulls in clj-miscutil 0.3
+                 [org.bituf/clj-dbcp      "0.5"]]
+  :dev-dependencies [[org.clojure/clojure "1.2.0"]]
+  :eval-in-leiningen true)

src/leiningen/lb.clj

 (ns leiningen.lb
-  (:require [clojure.contrib.duck-streams :as ds]))
+  (:import
+    (java.util.regex Pattern)
+    (javax.sql       DataSource))
+  (:require
+    [clojure.java.io :as io]
+    [clojure.string  :as sr]
+    [clojure.pprint  :as pp]
+    [org.bituf.clj-miscutil  :as mu]
+    [org.bituf.clj-dbcp      :as cp]
+    [org.bituf.clj-dbspec    :as sp]
+    [org.bituf.clj-liquibase :as lb]
+    [org.bituf.clj-liquibase.change :as ch])
+  (:use
+    [leiningen.compile   :only [eval-in-project]]))
 
-(def *version* "0.1 (uses Liquibase 2.0-RC6)")
+
+(def ^{:doc "Version of Lein-LB plugin"}
+      version [0 2])
+
 
 (defn help
   []
-  (println
-    (str "
-Lein-LB - Lein Liquibase plugin " *version* "
+  (println "The following commands are available
+help       - Shows this help screen
+version    - Shows version information
+dbcp-props - Prints a sample `clj-dbcp.properties` file
+update     - Updates the database
+rollback   - Rolls back database
+tag        - Tags the database
+dbdoc      - Generates documentation for database/changelogs
 
-For help on Liquibase commands:
+For help on individual command, append with `--help`, e.g.:
+lein lb update --help"))
 
-    $ lein lb help
 
-For help on Lein-LB commands (notice that the command begins with a colon):
+(defn ^String as-string
+  [s]
+  (if (keyword? s) (name s)
+    (str s)))
 
-    $ lein lb :help
 
-List of Lein-LB commands (they all begin with a colon):
+(defn opt?
+  [^String s] {:pre [(string? s)]}
+  (some #(re-matches % s) [(re-pattern "--.+")
+                           (re-pattern "-.+")]))
 
-    :help               prints this help screen
-    :sample-properties  prints sample properties - put in liquibase.properties
-"
-      )))
 
+(defn opt-string
+  ([^String elem] {:post [(string? %)]
+                   :pre  [(string? elem)]}
+    (format (if (> (count elem) 1)
+              "--%s"
+              "-%s")
+      elem))
+  ([^String elem ^String value]
+    (format (if (> (count elem) 1)
+              "--%s=%s"
+              "-%s%s")
+      elem value)))
 
-(defn sample-props
-  []
-  (let [filename "/liquibase.properties"
-        istream  (.getResourceAsStream String filename) ; returns InputStream
-        owriter  (java.io.StringWriter.)]
-    (ds/copy istream owriter)
-    (println (.toString owriter))))
 
+(defn opt-pattern
+  [^String elem] {:post [(instance? Pattern %)]
+                  :pre  [(string? elem)]}
+  (re-pattern (opt-string elem "(.*)")))
 
-(defn execute-liquibase-cmd
-  "Execute Liquibase as if using command-line arguments (String array)"
+
+(defn opt-value
+  "Return option value
+  Example:
+    (opt-value (opt-pattern \"foo\") \"--foo=bar\")
+    => returns \"bar\"
+  See also: opt-pattern"
+  [^Pattern re ^String arg]
+  (second (re-matches re arg)))
+
+
+(defn noarg-pattern
+  [^String elem] {:pre [(string? elem)]}
+  (re-pattern (format (if (> (count elem) 1)
+                        "--%s"
+                        "-%s")
+                elem)))
+
+
+(def arg-types #{:with-arg :opt-arg :no-arg})
+
+
+(defn print-usage
+  "Print command usage"
+  [cmd-prefix spec]
+  (println "Usage: " cmd-prefix "<options>\n")
+  (mu/print-table
+    ["Option" "Must" "Description"]
+    (map (fn [row]
+           (let [[desc opt-type & keywds] row
+                  takes-arg (contains? #{:with-arg :opt-arg} opt-type)
+                  ks (map #(if takes-arg
+                             (opt-string (as-string %) "<Val>")
+                             (opt-string (as-string %)))
+                       keywds)]
+              [(mu/comma-sep-str ks)
+               (if (= :with-arg opt-type) "Yes" "...")
+               desc]))
+      spec))
+  (println))
+
+
+(defn parse-opts
+  "Spec can be:
+    [[docstring :opt-arg :profile  :p]
+     [docstring :no-arg  :sql-only :s]
+     [docstring :with-arg :a]]
+  `args` is a collection of argument bodies:
+    \"--foo=bar\" \"-fbar\" \"--simulate\" \"-s\"
+  Note: Evaluated every time"
+  [cmd-prefix args & spec]
+  {:post [(map? %)]
+   :pre  [(coll? spec)                   ; spec is a collection
+          (every? coll? spec)            ; spec is a collection of collections
+          (every? #(> (count %) 2) spec) ; each sub-coll must be 2 elements or more
+          (every? #(string? (first %)) spec)              ; 1st elem is a docstring
+          (every? #(contains? arg-types (second %)) spec) ; 2nd elem is a valid arg-type
+          ]}
+  (let [spec-opts  (map #(map as-string (drop 2 %)) spec)
+        rev-opts   (->> spec-opts
+                     (map (fn [opt-row]
+                            (let [sentinel (keyword (first opt-row))]
+                              (map #(array-map % sentinel) opt-row))))
+                     flatten
+                     (reduce into))
+        ;;
+        with-arg (map (partial drop 2)    (filter #(= (second %) :with-arg) spec))
+        opt-arg  (map (partial drop 2)    (filter #(= (second %) :opt-arg)  spec))
+        no-arg   (map (partial drop 2)    (filter #(= (second %) :no-arg)   spec))
+        ;; fn to convert arg into map entries
+        get-opts (fn [acc arg] {:post [(map? %)] :pre  [(map? acc)
+                                                        (string? arg)]}
+                   (or
+                     ;; with-arg and opt-arg
+                     (some (fn [row]
+                             (some #(let [v (-> %
+                                              as-string
+                                              opt-pattern
+                                              (opt-value arg))]
+                                      (and v
+                                        (into acc
+                                          {(get rev-opts (as-string %)) v})))
+                               row))
+                       (into with-arg opt-arg))
+                     ;; no-arg
+                     (some (fn [row]
+                             (some (fn [opt]
+                                     (if (opt? arg)
+                                       (if (re-matches (noarg-pattern
+                                                         (as-string opt)) arg)
+                                         (into acc
+                                           {(get rev-opts (as-string opt)) nil}))
+                                       (if (contains? acc :more)
+                                         (into acc :more (cons arg (:more acc)))
+                                         {:more [arg]})))
+                               row))
+                       no-arg)
+                     ;; special or bad args
+                     (if (some #(= arg %) ["--help" "-h" "/?"])
+                       (do (print-usage cmd-prefix spec)
+                         {:help nil})
+                       (throw (IllegalArgumentException.
+                                (str "Illegal option: " arg))))))]
+    (let [opt-map   (reduce get-opts {} args)
+          with-arg? (fn []
+                      (every? (fn [row]
+                                (some (fn [opt]
+                                        (some #(re-matches
+                                                 (opt-pattern (as-string opt))
+                                                 (as-string %))
+                                          args))
+                                  row))
+                        with-arg))]
+      (cond
+        ;; ignore validations if help was sought
+        (contains?
+          opt-map :help)  opt-map
+        ;; ensure that `with-arg` options are supplied
+        (not (with-arg?)) (let [optfn #(let [x (as-string %)]
+                                         (if (> (count x) 1)
+                                           (str "--" x) (str "-" x)))
+                                optsr #(format "Either of %s\n"
+                                         (mu/comma-sep-str (map optfn %)))]
+                            (throw (IllegalArgumentException.
+                                     (str "Must supply the following:\n"
+                                       (apply str (map optsr with-arg))))))
+        :else             opt-map))))
+
+
+(defn resolve-var
+  "Given a qualified/un-qualified var name (string), resolve and return value.
+  Throw NullPointerException if var cannot be resolved."
+  [^String var-name] {:pre [(string? var-name)]}
+  @(let [tokens (sr/split var-name #"/")
+         var-ns (first tokens)]
+     (when (and (> (count tokens) 1)
+             (not (find-ns (symbol var-ns))))
+       (require (symbol var-ns)))
+     (resolve (symbol var-name))))
+
+
+(defn make-datasource
+  "Given profile name (which could be nil for default), return DataSource"
+  [profile] {:post [(instance? DataSource %)]
+             :pre  [(or (nil? profile)
+                      (string? profile))]}
+  (if profile
+    (cp/make-datasource-from-properties
+      (cp/load-datasource-args profile))
+    (cp/make-datasource-from-properties)))
+
+
+(defn ctx-list
+  "Generate context list from a given comma-separated context list (string)"
+  [contexts] {:post [(vector? %)]
+              :pre  [(or (nil? contexts)
+                       (string? contexts))]}
+  (if contexts
+    (sr/split contexts #",")
+    []))
+
+
+(defn parse-update-args
   [& args]
-  (let [argv (into-array String args)]
-    (liquibase.integration.commandline.Main/main argv)))
+  (parse-opts "lein lb update"
+    args
+    ["Changelog var name to apply update on"  :with-arg :changelog :c]
+    ["Clj-DBCP profile name (or default)"     :opt-arg  :profile   :p]
+    ["How many Changesets to apply update on" :opt-arg  :chs-count :n]
+    ["Contexts (comma separated)"             :opt-arg  :contexts  :t]
+    ["Only generate SQL, do not commit"       :no-arg   :sql-only  :s]))
+
+
+(defn update
+  [& args]
+  (let [opt (apply parse-update-args args)]
+    (when-not (contains? opt :help)
+      (let [changelog  (resolve-var (:changelog opt))
+            profile    (:profile   opt)
+            chs-count  (:chs-count opt)
+            contexts   (:contexts  opt)
+            sql-only   (contains? opt :sql-only)]
+        (sp/with-dbspec (cp/db-spec (make-datasource profile))
+          (lb/with-lb
+            (if chs-count
+              (let [chs-num (Integer/parseInt chs-count)]
+                (if sql-only
+                  (lb/update-by-count changelog chs-num (ctx-list contexts) *out*)
+                  (lb/update-by-count changelog chs-num (ctx-list contexts))))
+              (if sql-only
+                (lb/update changelog (ctx-list contexts) *out*)
+                (lb/update changelog (ctx-list contexts))))))))))
+
+
+(defn parse-rollback-args
+  [& args]
+  (parse-opts "lein lb rollback"
+    args
+    ["Changelog var name to apply rollback on"   :with-arg :changelog :c]
+    ["Clj-DBCP profile name (or default)"        :opt-arg  :profile   :p]
+    ["How many Changesets to rollback"           :opt-arg  :chs-count :n]
+    ["Which tag to rollback to"                  :opt-arg  :tag       :g]
+    ["Rollback ISO-date (yyyy-MM-dd'T'HH:mm:ss)" :opt-arg  :date      :d]
+    ["Contexts (comma separated)"                :opt-arg  :contexts  :t]
+    ["Only generate SQL, do not commit"          :no-arg   :sql-only  :s]))
+
+
+(defn rollback
+  [& args]
+  (let [opt (apply parse-rollback-args args)]
+    (when-not (contains? opt :help)
+      (let [changelog  (resolve-var (:changelog opt))
+            profile    (:profile   opt)
+            chs-count  (:chs-count opt)
+            tag        (:tag       opt)
+            date       (:date      opt)
+            c-t-d      [chs-count tag date] ; either of 3 is required
+            contexts   (:contexts  opt)
+            sql-only   (contains? opt :sql-only)]
+        (when (not (= 1 (count (filter identity c-t-d))))
+          (throw
+            (IllegalArgumentException.
+              (format
+                "Expected only either of --chs-count/-n, --tag/-g and --date/-d
+arguments, but found %s"
+                (with-out-str (pp/pprint args))))))
+        (sp/with-dbspec (cp/db-spec (make-datasource profile))
+          (lb/with-lb
+            (cond
+              chs-count (let [chs-num (Integer/parseInt chs-count)]
+                          (if sql-only
+                            (lb/rollback-by-count changelog chs-num (ctx-list contexts) *out*)
+                            (lb/rollback-by-count changelog chs-num (ctx-list contexts))))
+              tag       (if sql-only
+                          (lb/rollback-to-tag changelog tag (ctx-list contexts) *out*)
+                          (lb/rollback-to-tag changelog tag (ctx-list contexts)))
+              date      (if sql-only
+                          (lb/rollback-to-date changelog (ch/iso-date date) (ctx-list contexts) *out*)
+                          (lb/rollback-to-date changelog (ch/iso-date date) (ctx-list contexts)))
+              :else     (throw
+                          (IllegalStateException.
+                            (format
+                              "Neither of changeset-count, tag and date found to
+roll back to: %s"
+                              (with-out-str (pp/pprint args))))))))))))
+
+
+(defn parse-tag-args
+  [& args]
+  (parse-opts "lein lb tag"
+    args
+    ["Clj-DBCP profile name (or default)" :opt-arg  :profile   :p]
+    ["Tag name to apply"                  :with-arg :tag       :g]))
+
+
+(defn tag
+  "Tag the database manually (recommended: create a Change object of type tag)"
+  [& args]
+  (let [opt (apply parse-tag-args args)]
+    (when-not (contains? opt :help)
+      (let [profile   (:profile opt)
+            tag       (:tag     opt)]
+        (sp/with-dbspec (cp/db-spec (make-datasource profile))
+          (lb/with-lb
+            (lb/tag tag)))))))
+
+
+(defn parse-dbdoc-args
+  "Parse arguments for `dbdoc` command."
+  [& args]
+  (parse-opts "lein lb dbdoc"
+    args
+    ["Changelog var name to apply tag on"             :with-arg :changelog  :c]
+    ["Clj-DBCP profile name (default if unspecified)" :opt-arg  :profile    :p]
+    ["Output directory to generate doc files into"    :with-arg :output-dir :o]
+    ["Contexts (comma separated)"                     :opt-arg  :contexts   :t]))
+
+
+(defn dbdoc
+  "Generate database/changelog documentation"
+  [& args]
+  (let [opt (apply parse-dbdoc-args args)]
+    (when-not (contains? opt :help)
+      (let [changelog (resolve-var (:changelog opt))
+            profile   (:profile    opt)
+            out-dir   (:output-dir opt)
+            contexts  (:contexts   opt)]
+        (sp/with-dbspec (cp/db-spec (make-datasource profile))
+          (lb/with-lb
+            (lb/generate-doc changelog out-dir (ctx-list contexts))))))))
 
 
 ;; ----- Leiningen plugin command -----
 
+
+(defn prepare-args
+  [project ks sub-args] {:post [(vector? %)]
+                         :pre  [(map? project)
+                                (coll? sub-args)
+                                (not (map? sub-args))]}
+  (if (contains? project :lein-lb)
+    (let [lein-lb (:lein-lb project)]
+      (when (not (map? lein-lb))
+        (throw (IllegalArgumentException.
+                 "The :lein-lb key in project.clj must point to a map.")))
+      (when (not (every? keyword? (keys lein-lb)))
+        (throw (IllegalArgumentException.
+                 "All keys in the map under :lein-lb must be keywords")))
+      (when (not (every? #(or (string? %) (nil? %)) (vals lein-lb)))
+        (throw (IllegalArgumentException.
+                 "All values in the map under :lein-lb must be nil or string")))
+      (into (vec (map (fn [[k v]]
+                        (opt-string (as-string k) v))
+                   (select-keys lein-lb ks)))
+        sub-args))
+    sub-args))
+
+
+(defmacro eip
+  [project & body]
+  `(eval-in-project ~project
+     (mu/! ~@body) (fn [& args#] (pp/pprint args#))))
+
+
+(defn eip-fn
+  [project f args ks] {:pre [(map? project)
+                             (fn? f)
+                             (coll? ks)
+                             (coll? args)]}
+  (mu/!
+    (eip project (apply f (prepare-args project ks args)))))
+
+
 (defn lb
-  [& args]
+  [project & args]
   (let [argc (count args)
-        cmd  (first args)]
+        cmd  (or (first args) "")]
     ;; check for lein-lb commands
-    (cond
-      (empty? args)                (help)
-      (= cmd ":help")              (help)
-      (= cmd ":sample-properties") (sample-props)
-      :else                        (apply execute-liquibase-cmd args))))
+    (case (sr/lower-case cmd)
+      ""           (help)
+      "help"       (help)
+      "version"    (println (format "Lein-LB version %s"
+                              (apply str (interpose "." version))))
+      "dbcp-props" (println (slurp (io/resource "sample-clj-dbcp.properties")))
+      "update"     (eip-fn project update (rest args)
+                     [:changelog :profile :chs-count :contexts :sql-only])
+      "rollback"   (eip-fn project rollback (rest args)
+                     [:changelog :profile :chs-count :tag :date :contexts :sql-only])
+      "tag"        (eip-fn project tag (rest args)
+                     [:profile :tag])
+      "dbdoc"      (eip-fn project dbdoc (rest args)
+                     [:changelog :profile :output-dir :contexts])
+      (do
+        (println (format "Invalid command: %s" cmd))
+        (lb project "help")))))

src/liquibase.properties

-###
-### liquibase.properties (uncomment lines as required)
-###
-### More details here: http://www.liquibase.org/manual/command_line
-###
-
-
-## ----- Required Parameters -----
-
-## Migration file (XML)
-#changeLogFile=<path and filename>
-
-## Database username
-#username=<value>
-
-## Database password
-#password=<value>
-
-## Database URL
-#url=<value>
-
-
-## ----- Optional Parameters -----
-
-## Classpath containing migration files and JDBC Driver
-#classpath=<value>
-
-## Database driver class name
-#driver=<jdbc.driver.ClassName>
-
-
-## custom liquibase.database.Database implementation to use
-#databaseClass=<database.ClassName>
-
-## Default database schema to use
-#defaultSchemaName=<name>
-
-## ChangeSet contexts to execute
-#contexts=<value>
-
-## File with default option values (default: ./liquibase.properties)
-#defaultsFile=</path/to/file.properties>
-
-## Include the system classpath in the Liquibase classpath (default: true)
-#includeSystemClasspath=<true|false>
-
-## Prompt if non-localhost databases (default: false)
-#promptForNonLocalDatabase=<true|false>
-
-## Execution log level (debug, info, warning, severe, off)
-#logLevel=<level>
-
-## Log file
-#logFile=<file>
-
-## Overrides current date time function used in SQL. Useful for
-## unsupported databases.
-#currentDateTimeFunction=<value>
-
-
-## ----- Required Diff Parameters -----
-
-## Reference Database username
-#referenceUsername=<value>
-
-## Reference Database password
-#referencePassword=<value>
-
-## Reference Database URL
-#referenceUrl=<value>
-
-
-## ----- Optional Diff Parameters -----
-
-## Reference Database driver class name
-#referenceDriver=<jdbc.driver.ClassName>
-
-## Output data as CSV in the given directory
-#dataOutputDirectory=DIR
-
-
-## ----- Canned properties for different databases -----
-
-## MySQL
-#username=dbuser
-#password=secret
-#url=jdbc:mysql://localhost:3306/exampledb
-#driver=com.mysql.jdbc.Driver
-#defaultSchemaName=exampledb
-
-
-## PostgreSQL: s
-#username=dbuser
-#password=secret
-#url=jdbc:postgresql://localhost:5432/exampledb
-#driver=org.postgresql.Driver
-#defaultSchemaName=exampledb
-
-
-## Sybase
-#username=dbuser
-#password=secret
-#url=jdbc:jtds:sybase://localhost:7100/exampledb
-#driver=org.postgresql.Driver
-#defaultSchemaName=exampledb
-
-
-## MS SQL Server
-#username=dbuser
-#password=secret
-#url=jdbc:jtds:sqlserver://localhost:1433/exampledb
-#driver=org.postgresql.Driver
-#defaultSchemaName=exampledb

src/sample-clj-dbcp.properties

+# This is a sample Clj-DBCP properties file for creating data sources. Name
+# this file as 'clj-dbcp.properties' and put it in classpath-root OR in the
+# current directory (i.e. project folder if that is where you run cmds from).
+
+# Notice that this file is divided in sections, where each section represents
+# a profile (e.g. Dev, QA, Staging, Production etc.) To specify the default
+# profile mention it using the `default=<name>` property. Each profile property
+# should have `<profile>.` as a prefix for all property names in the profile.
+
+default=dev2
+
+mydev.odbc-dsn=cubrid-bituf
+#mydev.username=
+#mydev.password=
+
+dev2.db-type=h2-memory
+
+dev3.db-type=h2-filesystem
+# the parameter below is optional (`default` is assumed if nothing specified)
+dev3.db-path=/home/user/eddie/shopping
+
+# MySQL profile
+# hostport can also be in the form "localhost:3306"
+mysql.db-type=mysql
+mysql.hostport=localhost
+mysql.database=employee
+mysql.username=root
+mysql.password=s3cr3t
+
+# Supported database types (as in the example above) are:
+#
+# -- in-memory (you only need to pass the `db-type` parameter)
+# derby-memory, h2-memory, hsql-memory, sqlite-memory
+#
+# -- filesystem (you can optionally pass DB name/path as per database)
+# derby-filesystem, h2-filesystem, hsql-filesystem, sqlite-filesystem
+#
+# -- network
+# derby-network, h2-network, hsql-network
+# cubrid, firebird, mysql, pgsql, oracle, db2, sqlserver, sybase
+
+# Raw JDBC profile
+jdbc.classname=org.h2.Driver
+jdbc.jdbc-url=jdbc:h2:mem:sample
+jdbc.username=sa
+jdbc.password=
+# the params below are optional (and available in all profile types)
+jdbc.max-active=10
+jdbc.min-max-idle=2,4
+jdbc.validation-query=SELECT 1

test/leiningen/test/lb.clj

-(ns leiningen.test.lb
-  (:use [leiningen.lb] :reload)
-  (:use [clojure.test]))
-
-(deftest replace-me ;; FIXME: write
-  (is false "No tests have been written."))

test/leiningen/test_lb.clj

+(ns leiningen.test-lb
+  (:require
+    [leiningen.lb :as ll])
+  (:use [clojure.test]))
+
+
+(deftest test-update-args
+  (testing "update args"
+    (let [a {:profile "pp" :changelog "x"}]
+      (is (= a (ll/parse-update-args "--profile=pp"   "-cx")) "--profile")
+      (is (= a (ll/parse-update-args "-ppp"           "-cx")) "-p"))
+    (let [a {:changelog "foo.db/default"}]
+      (is (= a (ll/parse-update-args "--changelog=foo.db/default")) "--changelog")
+      (is (= a (ll/parse-update-args "-cfoo.db/default"))           "-c"))
+    (let [a {:chs-count "5" :changelog "x"}]
+      (is (= a (ll/parse-update-args "--chs-count=5"  "-cx")) "--chs-count")
+      (is (= a (ll/parse-update-args "-n5"            "-cx")) "-n"))
+    (let [a {:contexts "a,b" :changelog "x"}]
+      (is (= a (ll/parse-update-args "--contexts=a,b" "-cx")) "--contexts")
+      (is (= a (ll/parse-update-args "-ta,b"          "-cx")) "-t"))
+    (let [a {:sql-only nil :changelog "x"}]
+      (is (= a (ll/parse-update-args "--sql-only"     "-cx")) "--sql-only")
+      (is (= a (ll/parse-update-args "-s"             "-cx")) "-s"))
+    (let [a {:profile   "pp"
+             :changelog "foo.db/default"
+             :chs-count "5"
+             :contexts  "a,b"
+             :sql-only  nil}]
+      (is (= a (ll/parse-update-args
+                 "--profile=pp"
+                 "--changelog=foo.db/default"
+                 "--chs-count=5"
+                 "--contexts=a,b"
+                 "--sql-only"))      "all combined (full version)")
+      (is (= a (ll/parse-update-args
+                 "-ppp"
+                 "-cfoo.db/default"
+                 "-n5"
+                 "-ta,b"
+                 "-s"))              "all combined (short version)")
+      (is (thrown? IllegalArgumentException (ll/parse-update-args "--bad")))
+      (is (= {:help nil} (ll/parse-update-args "--help"))))))
+
+
+(deftest test-rollback-args
+  (testing "rollback args"
+    (let [a {:profile "pp" :changelog "x"}]
+      (is (= a (ll/parse-rollback-args "--profile=pp"      "-cx")) "--profile")
+      (is (= a (ll/parse-rollback-args "-ppp"              "-cx")) "-p"))
+    (let [a {:changelog "foo.db/default"}]
+      (is (= a (ll/parse-rollback-args "--changelog=foo.db/default")) "--changelog")
+      (is (= a (ll/parse-rollback-args "-cfoo.db/default"))           "-c"))
+    (let [a {:chs-count "5" :changelog "x"}]
+      (is (= a (ll/parse-rollback-args "--chs-count=5"     "-cx")) "--chs-count")
+      (is (= a (ll/parse-rollback-args "-n5"               "-cx")) "-n"))
+    (let [a {:tag "v2.0" :changelog "x"}]
+      (is (= a (ll/parse-rollback-args "--tag=v2.0"        "-cx")) "--tag")
+      (is (= a (ll/parse-rollback-args "-gv2.0"            "-cx")) "-g"))
+    (let [a {:date "2011-02-26" :changelog "x"}]
+      (is (= a (ll/parse-rollback-args "--date=2011-02-26" "-cx")) "--date")
+      (is (= a (ll/parse-rollback-args "-d2011-02-26"      "-cx")) "-d"))
+    (let [a {:contexts "a,b" :changelog "x"}]
+      (is (= a (ll/parse-rollback-args "--contexts=a,b"    "-cx")) "--contexts")
+      (is (= a (ll/parse-rollback-args "-ta,b"             "-cx")) "-t"))
+    (let [a {:sql-only nil :changelog "x"}]
+      (is (= a (ll/parse-rollback-args "--sql-only"        "-cx")) "--sql-only")
+      (is (= a (ll/parse-rollback-args "-s"                "-cx")) "-s"))
+    (let [a {:profile   "pp"
+             :changelog "foo.db/default"
+             :chs-count "5"
+             :contexts  "a,b"
+             :sql-only  nil}]
+      (is (= a (ll/parse-rollback-args
+                 "--profile=pp"
+                 "--changelog=foo.db/default"
+                 "--chs-count=5"
+                 "--contexts=a,b"
+                 "--sql-only"))      "all combined (full version)")
+      (is (= a (ll/parse-rollback-args
+                 "-ppp"
+                 "-cfoo.db/default"
+                 "-n5"
+                 "-ta,b"
+                 "-s"))              "all combined (short version)")
+      (is (thrown? IllegalArgumentException (ll/parse-rollback-args "--bad")))
+      (is (= {:help nil} (ll/parse-rollback-args "--help"))))))
+
+
+(deftest test-tag-args
+  (testing "rollback args"
+    (let [a {:profile "pp" :tag "y"}]
+      (is (= a (ll/parse-tag-args "--profile=pp"      "-gy")) "--profile")
+      (is (= a (ll/parse-tag-args "-ppp"              "-gy")) "-p"))
+    (let [a {:tag "v2.0"}]
+      (is (= a (ll/parse-tag-args "--tag=v2.0")) "--tag")
+      (is (= a (ll/parse-tag-args "-gv2.0"))     "-g"))
+    (let [a {:profile   "pp"
+             :tag       "foo"}]
+      (is (= a (ll/parse-tag-args
+                 "--profile=pp"
+                 "--tag=foo"))      "all combined (full version)")
+      (is (= a (ll/parse-tag-args
+                 "-ppp"
+                 "-gfoo"))          "all combined (short version)")
+      (is (thrown? IllegalArgumentException (ll/parse-tag-args "--bad")))
+      (is (= {:help nil} (ll/parse-tag-args "--help"))))))
+
+
+(deftest test-dbdoc-args
+  (testing "dbdoc args"
+    (let [a {:profile "pp" :output-dir "y" :changelog "x"}]
+      (is (= a (ll/parse-dbdoc-args "--profile=pp"      "-cx" "-oy")) "--profile")
+      (is (= a (ll/parse-dbdoc-args "-ppp"              "-cx" "-oy")) "-p"))
+    (let [a {:output-dir "y" :changelog "foo.db/default"}]
+      (is (= a (ll/parse-dbdoc-args "--changelog=foo.db/default" "-oy")) "--changelog")
+      (is (= a (ll/parse-dbdoc-args "-cfoo.db/default"           "-oy")) "-c"))
+    (let [a {:output-dir "foo/bar" :changelog "x"}]
+      (is (= a (ll/parse-dbdoc-args "--output-dir=foo/bar" "-cx")) "--output-dir")
+      (is (= a (ll/parse-dbdoc-args "-ofoo/bar"            "-cx")) "-o"))
+    (let [a {:profile    "pp"
+             :changelog  "foo.db/default"
+             :output-dir "foo"}]
+      (is (= a (ll/parse-dbdoc-args
+                 "--profile=pp"
+                 "--changelog=foo.db/default"
+                 "--output-dir=foo"))         "all combined (full version)")
+      (is (= a (ll/parse-dbdoc-args
+                 "-ppp"
+                 "-cfoo.db/default"
+                 "-ofoo"))                    "all combined (short version)")
+      (is (thrown? IllegalArgumentException (ll/parse-dbdoc-args "--bad")))
+      (is (= {:help nil} (ll/parse-dbdoc-args "--help"))))))
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.