[dashboard] Add multiple revisions

As IP grows and updated, it is needed to have multiple versions of IPs
to be managed in the dashboard. For instance, as UART, GPIO, RV_TIMER
were signed off, their versions shall be increased when the logic design
is changed.

This change introduces `revisions` field in the project Hjson file. If
this field is defined, the dashboard prints out new format.

Revised `uart` IP to show the example.

Signed-off-by: Eunchan Kim <eunchan@opentitan.org>
diff --git a/hw/ip/uart/data/uart.prj.hjson b/hw/ip/uart/data/uart.prj.hjson
index 3050f5e..de0b3b6 100644
--- a/hw/ip/uart/data/uart.prj.hjson
+++ b/hw/ip/uart/data/uart.prj.hjson
@@ -4,9 +4,14 @@
 
 {
     name:               "uart",
-    version:            "1.0",
-    life_stage:         "L2",
-    design_stage:       "D3",
-    verification_stage: "V3",
-    notes:              "signoff commit id: 92e4298f8c2de268b2420a2c16939cd0784f1bf8"
+    revisions: [
+      {
+        version:            "1.0",
+        life_stage:         "L2",
+        design_stage:       "D3",
+        verification_stage: "V3",
+        commit_id:          "92e4298f8c2de268b2420a2c16939cd0784f1bf8",
+        notes:              ""
+      }
+    ]
 }
diff --git a/site/docs/assets/scss/_markdown.scss b/site/docs/assets/scss/_markdown.scss
index e0f3c48..769d1c1 100644
--- a/site/docs/assets/scss/_markdown.scss
+++ b/site/docs/assets/scss/_markdown.scss
@@ -136,7 +136,10 @@
 
 // Dashboard design
 table.hw-project-dashboard {
-    td.hw-stage {
-        text-align: center;
+    td {
+        vertical-align: middle;
+        &.hw-stage {
+            text-align: center;
+        }
     }
 }
diff --git a/site/docs/layouts/shortcodes/dashboard.html b/site/docs/layouts/shortcodes/dashboard.html
index af00d2a..92d637a 100644
--- a/site/docs/layouts/shortcodes/dashboard.html
+++ b/site/docs/layouts/shortcodes/dashboard.html
@@ -6,6 +6,7 @@
 			<th>Life Stage</th>
 			<th>Design Stage</th>
 			<th>Verification Stage</th>
+			<th>Commit ID</th>
 			<th>Notes</th>
 		</tr>
 	</thead>
diff --git a/util/dashboard/dashboard_validate.py b/util/dashboard/dashboard_validate.py
index 099453e..e323651 100644
--- a/util/dashboard/dashboard_validate.py
+++ b/util/dashboard/dashboard_validate.py
@@ -38,6 +38,17 @@
     'notes': ['s', "random notes"],
 }
 
+entry_required = {
+    'version': ['s', "module version"],
+    'life_stage': ['s', "life stage of module"]
+}
+entry_optional = {
+    'design_stage': ['s', "design stage of module"],
+    'verification_stage': ['s', "verification stage of module"],
+    'commit_id': ['s', "Staged commit ID"],
+    'notes': ['s', "notes"],
+}
+
 
 def validate(regs):
     if not 'name' in regs:
@@ -45,8 +56,26 @@
         return 1
     component = regs['name']
 
-    error = check_keys(regs, field_required, field_optional, component)
+    # If `revisions` is not in the object keys, the tool runs previous
+    # version checker, which has only one version entry.
+    if not "revisions" in regs:
+        error = check_keys(regs, field_required, field_optional, component)
+        if (error > 0):
+            log.error("Component has top level errors. Aborting.")
+        return error
+
+    # Assumes `revisions` field exists in the Hjson object.
+    # It iterates the entries in the `revisions` group.
+    error = 0
+    if not isinstance(regs['revisions'], list):
+        error += 1
+        log.error("`revisions` field should be a list of version entries")
+        return error
+
+    for rev in regs['revisions']:
+        error += check_keys(rev, entry_required, entry_optional, component)
+
     if (error > 0):
-        log.error("Component has top level errors. Aborting.")
+        log.error("Component has errors in revision field. Aborting.")
 
     return error
diff --git a/util/dashboard/gen_dashboard_entry.py b/util/dashboard/gen_dashboard_entry.py
index d5aff14..4b6b71e 100644
--- a/util/dashboard/gen_dashboard_entry.py
+++ b/util/dashboard/gen_dashboard_entry.py
@@ -50,6 +50,17 @@
     else:
         log.fail("hjson file import failed\n")
 
+    # If `revisions` field doesn't exist, the tool assumes the Hjson
+    # as the previous project format, which has only one version entry.
+    if not "revisions" in obj:
+        print_version1_format(obj, outfile)
+    else:
+        print_multiversion_format(obj, outfile)
+        return
+
+
+# Version 1 (single version) format
+def print_version1_format(obj, outfile):
     life_stage = obj['life_stage']
     life_stage_mapping = convert_stage(obj['life_stage'])
 
@@ -80,6 +91,10 @@
     else:
         genout(outfile,
                     "        <td>&nbsp;</td>\n")
+
+    # Empty commit ID
+    genout(outfile, "        <td>&nbsp;</td>\n")
+
     if 'notes' in obj:
         genout(outfile,
                     "        <td>" + mk.markdown(obj['notes']) + "</td>\n")
@@ -88,7 +103,70 @@
                     "        <td>&nbsp;</td>\n")
     genout(outfile, "      </tr>\n")
     # yapf: enable
-    return
+
+
+def print_multiversion_format(obj, outfile):
+    # Sort the revision list based on the version field.
+    # TODO: If minor version goes up gte than 10?
+    revisions = sorted(obj["revisions"], key=lambda x: x["version"])
+    outstr = ""
+    for i, rev in enumerate(revisions):
+        outstr += "      <tr>\n"
+
+        # If only one entry in `revisions`, no need of `rowspan`.
+        if len(revisions) == 1:
+            outstr += "        <td class='fixleft'>"
+            outstr += html.escape(obj['name']) + "</td>\n"
+        # Print out the module name in the first entry only
+        elif i == 0:
+            outstr += "        <td class='fixleft' rowspan='{}'>".format(
+                len(revisions))
+            outstr += html.escape(obj['name']) + "</td>\n"
+
+        # Version
+        outstr += "        <td class=\"hw-stage\">"
+        outstr += html.escape(rev['version']) + "</td>\n"
+
+        # Life Stage
+        life_stage = rev['life_stage']
+        life_stage_mapping = convert_stage(rev['life_stage'])
+
+        outstr += "        <td class=\"hw-stage\"><span class='hw-stage' title='"
+        outstr += html.escape(life_stage_mapping) + "'>"
+        outstr += html.escape(life_stage) + "</span></td>\n"
+
+        if life_stage != 'L0' and 'design_stage' in rev:
+            design_stage_mapping = convert_stage(rev['design_stage'])
+            outstr += "        <td class=\"hw-stage\"><span class='hw-stage' title='"
+            outstr += html.escape(design_stage_mapping) + "'>"
+            outstr += html.escape(rev['design_stage']) + "</span></td>\n"
+        else:
+            outstr += "        <td>&nbsp;</td>\n"
+
+        if life_stage != 'L0' and 'verification_stage' in rev:
+            verification_stage_mapping = convert_stage(
+                rev['verification_stage'])
+            outstr += "        <td class=\"hw-stage\"><span class='hw-stage' title='"
+            outstr += html.escape(verification_stage_mapping) + "'>"
+            outstr += html.escape(rev['verification_stage']) + "</span></td>\n"
+        else:
+            outstr += "        <td>&nbsp;</td>\n"
+
+        if 'commit_id' in rev:
+            outstr += "        <td class=\"hw-stage\">"
+            outstr += "<a href='https://github.com/lowrisc/opentitan/tree/{}'>{}</a>".format(
+                rev['commit_id'], rev['commit_id'][0:7])
+            outstr += "</td>\n"
+        else:
+            outstr += "        <td>&nbsp;</td>\n"
+
+        if 'notes' in rev:
+            outstr += "        <td>" + mk.markdown(rev['notes']) + "</td>\n"
+        else:
+            outstr += "        <td>&nbsp;</td>\n"
+        outstr += "      </tr>\n"
+
+    genout(outfile, outstr)
 
 
 # Create table of hardware specifications
@@ -102,7 +180,7 @@
     if dashboard_validate.validate(obj) == 0:
         log.info("Generated dashboard object for " + str(hjson_path))
     else:
-        log.fail("hjson file import failed")
+        log.error("hjson file import failed")
 
     # create design spec and DV plan references, check for existence below
     design_spec_md = re.sub(r'/data/', '/doc/',