Add rev-list wpt subcommad (#19536)

Adds the rev-list subcommand for the wpt tool which is basically a
wrapper for the `git for-each-ref` command filter for `merge_pr_*` tags.

The `wpt rev-list` will show the more recent revision closer to each
epoch step for the epoch size  defined in the command line.

This command is executed for a specific timestamp which is (now -
threshold). This threshold is set to 600 seconds.

Iterates the tagged revisions in descending order finding the more
recent commit still older than a "cutoff_date" value.
When a commit is found "cutoff_date" is set to a new value multiplier of
"epoch" but still below of the date of the current commit found.
This is needed to deal with intervals where no candidates were found
for the current "epoch" and the next candidate found is yet below
the lower values of the interval (it is the case of J and I for the
interval between Wed and Tue, in the example). The algorithm fix
the next "cutoff_date" value based on the date value of the current one
skipping the intermediate values.
The loop ends once we reached the required number of revisions to return
or the are no more tagged revisions.

    Fri   Sat   Sun   Mon   Tue   Wed   Thu   Fri   Sat
     |     |     |     |     |     |     |     |     |
  -A---B-C---DEF---G---H--IJ----------K-----L-M----N--O--
                                                        ^
                                                       now
  Expected result: N,M,K,J,H,G,F,C,A
diff --git a/tools/wpt/commands.json b/tools/wpt/commands.json
index 178eda9..60fe162 100644
--- a/tools/wpt/commands.json
+++ b/tools/wpt/commands.json
@@ -63,6 +63,13 @@
     "help": "Print branch point from master",
     "virtualenv": false
   },
+  "rev-list": {
+    "path": "revlist.py",
+    "script": "run_rev_list",
+    "parser": "get_parser",
+    "help": "List tagged revisions at regular intervals",
+    "virtualenv": false
+  },
   "install-android-emulator": {
     "path": "android.py",
     "script": "run_install",
diff --git a/tools/wpt/revlist.py b/tools/wpt/revlist.py
new file mode 100644
index 0000000..f750311
--- /dev/null
+++ b/tools/wpt/revlist.py
@@ -0,0 +1,118 @@
+import argparse
+import logging
+import os
+import time
+from tools.wpt.testfiles import get_git_cmd
+
+here = os.path.dirname(__file__)
+wpt_root = os.path.abspath(os.path.join(here, os.pardir, os.pardir))
+
+logger = logging.getLogger()
+
+MYPY = False
+if MYPY:
+    # MYPY is set to True when run under Mypy.
+    from typing import Any
+    from typing import Dict
+    from typing import List
+    from typing import Text
+
+
+def calculate_cutoff_date(until, epoch, offset):
+    return ((((until - offset) // epoch)) * epoch) + offset
+
+
+def parse_epoch(string):
+    # type: (str) -> int
+    UNIT_DICT = {"h": 3600, "d": 86400, "w": 604800}
+    base = string[:-1]
+    unit = string[-1:]
+    if base.isdigit() and unit in UNIT_DICT:
+        return int(base) * UNIT_DICT[unit]
+    raise argparse.ArgumentTypeError('must be digits followed by h/d/w')
+
+
+def get_tagged_revisions(pattern):
+    # type: (bytes) -> List[..., Dict]
+    '''
+    Returns the tagged revisions indexed by the committer date.
+    '''
+    git = get_git_cmd(wpt_root)
+    args = [
+        pattern,
+        '--sort=-committerdate',
+        '--format=%(refname:lstrip=2) %(objectname) %(committerdate:raw)',
+        '--count=100000'
+    ]
+    for line in git("for-each-ref", *args).splitlines():
+        tag, commit, date, _ = line.split(" ")
+        date = int(date)
+        yield tag, commit, date
+
+
+def list_tagged_revisons(epoch, max_count):
+    # type: (**Any) -> List[Text]
+    logger.debug("list_tagged_revisons(%s, %s)" % (epoch, max_count))
+    # Set an offset to start to count the the weekly epoch from
+    # Monday 00:00:00. This is particularly important for the weekly epoch
+    # because fix the start of the epoch to Monday. This offset is calculated
+    # from Thursday, 1 January 1970 0:00:00 to Monday, 5 January 1970 0:00:00
+    epoch_offset = 345600
+    # "epoch_threshold" set a safety margin after this time it is fine to
+    # consider that any tags are created and pushed.
+    epoch_threshold = 600
+    epoch_until = int(time.time()) - epoch_threshold
+    count = 0
+
+    # Iterates the tagged revisions in descending order finding the more
+    # recent commit still older than a "cutoff_date" value.
+    # When a commit is found "cutoff_date" is set to a new value multiplier of
+    # "epoch" but still below of the date of the current commit found.
+    # This needed to deal with intervals where no candidates were found
+    # for the current "epoch" and the next candidate found is yet below
+    # the lower values of the interval (it is the case of J and I for the
+    # interval between Wed and Tue, in the example). The algorithm fix
+    # the next "cutoff_date" value based on the date value of the current one
+    # skipping the intermediate values.
+    # The loop ends once we reached the required number of revisions to return
+    # or the are no more tagged revisions or the cutoff_date reach zero.
+    #
+    #   Fri   Sat   Sun   Mon   Tue   Wed   Thu   Fri   Sat
+    #    |     |     |     |     |     |     |     |     |
+    # -A---B-C---DEF---G---H--IJ----------K-----L-M----N--O--
+    #                                                       ^
+    #                                                      now
+    # Expected result: N,M,K,J,H,G,F,C,A
+
+    cutoff_date = calculate_cutoff_date(epoch_until, epoch, epoch_offset)
+    for _, commit, date in get_tagged_revisions("refs/tags/merge_pr_*"):
+        if count >= max_count:
+            return
+        if date < cutoff_date:
+            print(commit)
+            count += 1
+            cutoff_date = calculate_cutoff_date(date, epoch, epoch_offset)
+
+
+def get_parser():
+    # type: () -> argparse.ArgumentParser
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--epoch",
+                        default="1d",
+                        type=parse_epoch,
+                        help="regular interval of time selected to get the "
+                             "tagged revisions. Valid values are digits "
+                             "followed by h/d/w (e.x. 9h, 9d, 9w ...) where "
+                             "the mimimun selectable interval is one hour "
+                             "(1h)")
+    parser.add_argument("--max-count",
+                        default=1,
+                        type=int,
+                        help="maximum number of revisions to be returned by "
+                             "the command")
+    return parser
+
+
+def run_rev_list(**kwargs):
+    # type: (**Any) -> None
+    list_tagged_revisons(kwargs["epoch"], kwargs["max_count"])
diff --git a/tools/wpt/tests/test_revlist.py b/tools/wpt/tests/test_revlist.py
new file mode 100644
index 0000000..7b13106
--- /dev/null
+++ b/tools/wpt/tests/test_revlist.py
@@ -0,0 +1,15 @@
+from tools.wpt import revlist
+
+
+def test_calculate_cutoff_date():
+    assert revlist.calculate_cutoff_date(3601, 3600, 0) == 3600
+    assert revlist.calculate_cutoff_date(3600, 3600, 0) == 3600
+    assert revlist.calculate_cutoff_date(3599, 3600, 0) == 0
+    assert revlist.calculate_cutoff_date(3600, 3600, 1) == 1
+    assert revlist.calculate_cutoff_date(3600, 3600, -1) == 3599
+
+
+def test_parse_epoch():
+    assert revlist.parse_epoch(b"10h") == 36000
+    assert revlist.parse_epoch(b"10d") == 864000
+    assert revlist.parse_epoch(b"10w") == 6048000