rollers: Check if roll is backwards

Backwards rolls never happened, but they caused infra failures when
running `git log $OLD..$NEW`. Now rollers check if the roll is in
reverse and marks the run as successful. This is needed because
occasionally LUCI config changes result in luci-scheduler thinking a
roller is new and triggering 30 rolls that all fail. Now they'll still
be triggered but they won't do anything and then they'll pass.

Added this logic to the roll_message module, which is now renamed to
roll_util.

Tested by retriggering a past successful roll.
$ led get-build 8868516989406765792 | led edit-recipe-bundle | \
    led edit -p 'dry_run=true' | led launch
LUCI UI: https://ci.chromium.org/swarming/task/4ecf5393d5a23d10

Also tested by pausing a roller and manually triggering after another CL
went in.
$ led get-builder luci.pigweed.ci:pigweed-stm32-roller | \
    led edit-recipe-bundle | led edit -p dry_run=true | led launch
LUCI UI: https://ci.chromium.org/swarming/task/4ed09708b7433d10

Change-Id: I057272a5221626312a96b195eccdaf9dd8dc053d
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/18683
Reviewed-by: Oliver Newman <olivernewman@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/recipe_modules/roll_message/test_api.py b/recipe_modules/roll_message/test_api.py
deleted file mode 100644
index 257c2c3..0000000
--- a/recipe_modules/roll_message/test_api.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright 2020 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-#     https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-from recipe_engine import recipe_test_api
-
-
-class RollMessageTestApi(recipe_test_api.RecipeTestApi):
-
-  def commit(self, hash, message):
-    return ' '.join((hash, message))
-
-  def commit_data(self, *commits):
-    return self.step_data('roll message.git log',
-                          stdout=self.m.raw_io.output('\0'.join(commits)))
diff --git a/recipe_modules/roll_message/tests/full.py b/recipe_modules/roll_message/tests/full.py
deleted file mode 100644
index 6ac9595..0000000
--- a/recipe_modules/roll_message/tests/full.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright 2020 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-#     https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Full test of non-repo functionality of checkout module."""
-
-from recipe_engine.config import List
-from recipe_engine.recipe_api import Property
-
-DEPS = [
-    'fuchsia/status_check',
-    'pigweed/roll_message',
-    'recipe_engine/path',
-    'recipe_engine/properties',
-    'recipe_engine/raw_io',
-]
-
-PROPERTIES = {
-    'project_name': Property(kind=str),
-    'original_commits': Property(kind=List),
-    'old_revision': Property(kind=str),
-    'new_revision': Property(kind=str),
-}
-
-
-def RunSteps(  # pylint: disable=invalid-name
-    api, project_name, old_revision, new_revision):
-  proj_dir = api.path['start_dir'].join('project')
-  api.roll_message(project_name, proj_dir, old_revision, new_revision)
-
-
-def GenTests(api):  # pylint: disable=invalid-name
-  yield (
-      api.status_check.test('singlecommit_singleline')
-      + api.properties(
-          project_name='proj',
-          old_revision='0'*40,
-          new_revision='1'*40)
-      + api.roll_message.commit_data(
-          api.roll_message.commit('1'*40, 'foo\n\nbar'))
-  )
-
-  yield (
-      api.status_check.test('singlecommit_multiline')
-      + api.properties(
-          project_name='proj',
-          old_revision='0'*40,
-          new_revision='1'*40)
-      + api.roll_message.commit_data(
-          api.roll_message.commit('1'*40, 'foo\n\nbar\n\nBug: 123\nCC: foo'))
-  )
-
-  yield (
-      api.status_check.test('multicommit')
-      + api.properties(
-          project_name='proj',
-          old_revision='0'*40,
-          new_revision='4'*40)
-      + api.roll_message.commit_data(
-          api.roll_message.commit('4'*40, 'xyz\n\nxyz'),
-          api.roll_message.commit('3'*40, 'baz\n\nbaz'),
-          api.roll_message.commit('2'*40,
-                                  ''.join(x * 10 for x in '0123456789')),
-          api.roll_message.commit('1'*40, 'foo\n\nfoo'),
-      )
-  )
-
-  yield (
-      api.status_check.test('frombranch')
-      + api.properties(
-          project_name='proj',
-          old_revision='main',
-          new_revision='5'*40)
-      + api.roll_message.commit_data(
-          api.roll_message.commit('5'*40, 'five'),
-          api.roll_message.commit('4'*40, 'four'),
-          api.roll_message.commit('3'*40, 'three'),
-          api.roll_message.commit('2'*40, 'two'),
-          api.roll_message.commit('1'*40, 'one'),
-      )
-  )
diff --git a/recipe_modules/roll_message/__init__.py b/recipe_modules/roll_util/__init__.py
similarity index 100%
rename from recipe_modules/roll_message/__init__.py
rename to recipe_modules/roll_util/__init__.py
diff --git a/recipe_modules/roll_message/api.py b/recipe_modules/roll_util/api.py
similarity index 75%
rename from recipe_modules/roll_message/api.py
rename to recipe_modules/roll_util/api.py
index 9d76fd1..61b6188 100644
--- a/recipe_modules/roll_message/api.py
+++ b/recipe_modules/roll_util/api.py
@@ -11,7 +11,7 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-"""Creates commit messages for rolls."""
+"""Utility functions for rollers."""
 
 import pprint
 import re
@@ -68,8 +68,7 @@
   return re.match(r'^[0-9a-fA-F]{40}', value)
 
 
-class RollMessageApi(recipe_api.RecipeApi):
-  """Creates commit messages for rolls."""
+class RollUtilApi(recipe_api.RecipeApi):
 
   def _single_commit_roll_message(self, project_name, commit,
                                   old_revision, new_revision):
@@ -151,7 +150,7 @@
         commits.append(Commit(hash, message))
       return commits
 
-  def __call__(self, project_name, proj_dir, old_revision, new_revision):
+  def message(self, project_name, proj_dir, old_revision, new_revision):
     with self.m.step.nest('roll message'):
       commits = self._commits(proj_dir, old_revision, new_revision)
 
@@ -162,3 +161,38 @@
       else:
         return self._single_commit_roll_message(
             project_name, commits[0], old_revision, new_revision)
+
+  def check_roll_direction(self, git_dir, old, new,
+                           name='check roll direction'):
+    """Return if old is an ancestor of new (i.e., the roll moves forward)."""
+    if old == new:
+      with self.m.step.nest(name) as pres:
+        pres.step_summary_text = 'up-to-date'
+      return False
+
+    with self.m.context(git_dir):
+      step = self.m.git(
+          'merge-base',
+          '--is-ancestor',
+          old,
+          new,
+          name=name,
+          ok_ret=(0, 1),
+      )
+
+      step.presentation.step_summary_text = 'backward'
+      if step.exc_result.retcode == 0:
+        step.presentation.step_summary_text = 'forward'
+
+      return step.exc_result.retcode == 0
+
+  def backwards_roll_step(self, remote, old_revision, new_revision):
+    with self.m.step.nest('cancelling roll') as pres:
+      fmt = ('not updating from {old} to {new} because {old} is newer '
+             'than {new}')
+      if old_revision == new_revision:
+        fmt = 'not updating from {old} to {new} because they are identical'
+      pres.step_summary_text = fmt.format(old=old_revision[0:7],
+                                          new=new_revision[0:7])
+      pres.links[old_revision] = '{}/+/{}'.format(remote, old_revision)
+      pres.links[new_revision] = '{}/+/{}'.format(remote, new_revision)
diff --git a/recipe_modules/roll_util/test_api.py b/recipe_modules/roll_util/test_api.py
new file mode 100644
index 0000000..efb98a2
--- /dev/null
+++ b/recipe_modules/roll_util/test_api.py
@@ -0,0 +1,37 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+from recipe_engine import post_process, recipe_test_api
+
+
+class RollUtilTestApi(recipe_test_api.RecipeTestApi):
+
+  def commit(self, hash, message):
+    return ' '.join((hash, message))
+
+  def commit_data(self, *commits):
+    return self.step_data('roll message.git log',
+                          stdout=self.m.raw_io.output('\0'.join(commits)))
+
+  def cancelled(self):
+    return self.post_process(post_process.MustRunRE, '.*cancelling roll.*')
+
+  def not_cancelled(self):
+    return self.post_process(post_process.DoesNotRunRE, '.*cancelling roll.*')
+
+  def rolls_forward(self, prefix='', name='check roll direction'):
+    return self.step_data(prefix + name, retcode=0)
+
+  def rolls_backward(self, prefix='', name='check roll direction'):
+    return self.step_data(prefix + name, retcode=1)
diff --git a/recipe_modules/roll_util/tests/full.expected/backwards.json b/recipe_modules/roll_util/tests/full.expected/backwards.json
new file mode 100644
index 0000000..ba15f0b
--- /dev/null
+++ b/recipe_modules/roll_util/tests/full.expected/backwards.json
@@ -0,0 +1,28 @@
+[
+  {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "0000000000000000000000000000000000000000",
+      "1111111111111111111111111111111111111111"
+    ],
+    "cwd": "None",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@backward@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "cancelling roll",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@not updating from 0000000 to 1111111 because 0000000 is newer than 1111111@@@",
+      "@@@STEP_LINK@0000000000000000000000000000000000000000@remote/+/0000000000000000000000000000000000000000@@@",
+      "@@@STEP_LINK@1111111111111111111111111111111111111111@remote/+/1111111111111111111111111111111111111111@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/roll_message/tests/full.expected/frombranch.json b/recipe_modules/roll_util/tests/full.expected/frombranch.json
similarity index 87%
rename from recipe_modules/roll_message/tests/full.expected/frombranch.json
rename to recipe_modules/roll_util/tests/full.expected/frombranch.json
index 551863a..2b71c55 100644
--- a/recipe_modules/roll_message/tests/full.expected/frombranch.json
+++ b/recipe_modules/roll_util/tests/full.expected/frombranch.json
@@ -1,5 +1,19 @@
 [
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "main",
+      "5555555555555555555555555555555555555555"
+    ],
+    "cwd": "None",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipe_modules/roll_message/tests/full.expected/multicommit.json b/recipe_modules/roll_util/tests/full.expected/multicommit.json
similarity index 86%
rename from recipe_modules/roll_message/tests/full.expected/multicommit.json
rename to recipe_modules/roll_util/tests/full.expected/multicommit.json
index 0da38b4..5702231 100644
--- a/recipe_modules/roll_message/tests/full.expected/multicommit.json
+++ b/recipe_modules/roll_util/tests/full.expected/multicommit.json
@@ -1,5 +1,19 @@
 [
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "0000000000000000000000000000000000000000",
+      "4444444444444444444444444444444444444444"
+    ],
+    "cwd": "None",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipe_modules/roll_message/tests/full.expected/singlecommit_multiline.json b/recipe_modules/roll_util/tests/full.expected/singlecommit_multiline.json
similarity index 84%
rename from recipe_modules/roll_message/tests/full.expected/singlecommit_multiline.json
rename to recipe_modules/roll_util/tests/full.expected/singlecommit_multiline.json
index a0659bd..f0bbbcb 100644
--- a/recipe_modules/roll_message/tests/full.expected/singlecommit_multiline.json
+++ b/recipe_modules/roll_util/tests/full.expected/singlecommit_multiline.json
@@ -1,5 +1,19 @@
 [
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "0000000000000000000000000000000000000000",
+      "1111111111111111111111111111111111111111"
+    ],
+    "cwd": "None",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipe_modules/roll_message/tests/full.expected/singlecommit_singleline.json b/recipe_modules/roll_util/tests/full.expected/singlecommit_singleline.json
similarity index 83%
rename from recipe_modules/roll_message/tests/full.expected/singlecommit_singleline.json
rename to recipe_modules/roll_util/tests/full.expected/singlecommit_singleline.json
index 359066a..5cd6672 100644
--- a/recipe_modules/roll_message/tests/full.expected/singlecommit_singleline.json
+++ b/recipe_modules/roll_util/tests/full.expected/singlecommit_singleline.json
@@ -1,5 +1,19 @@
 [
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "0000000000000000000000000000000000000000",
+      "1111111111111111111111111111111111111111"
+    ],
+    "cwd": "None",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipe_modules/roll_util/tests/full.expected/up_to_date.json b/recipe_modules/roll_util/tests/full.expected/up_to_date.json
new file mode 100644
index 0000000..2d2d34f
--- /dev/null
+++ b/recipe_modules/roll_util/tests/full.expected/up_to_date.json
@@ -0,0 +1,20 @@
+[
+  {
+    "cmd": [],
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@up-to-date@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "cancelling roll",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@not updating from 1111111 to 1111111 because they are identical@@@",
+      "@@@STEP_LINK@1111111111111111111111111111111111111111@remote/+/1111111111111111111111111111111111111111@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/roll_util/tests/full.py b/recipe_modules/roll_util/tests/full.py
new file mode 100644
index 0000000..7318308
--- /dev/null
+++ b/recipe_modules/roll_util/tests/full.py
@@ -0,0 +1,121 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Full test of non-repo functionality of checkout module."""
+
+from recipe_engine.config import List
+from recipe_engine.recipe_api import Property
+
+DEPS = [
+    'fuchsia/status_check',
+    'pigweed/roll_util',
+    'recipe_engine/path',
+    'recipe_engine/properties',
+    'recipe_engine/raw_io',
+]
+
+PROPERTIES = {
+    'project_name': Property(kind=str),
+    'original_commits': Property(kind=List),
+    'old_revision': Property(kind=str),
+    'new_revision': Property(kind=str),
+}
+
+
+def RunSteps(  # pylint: disable=invalid-name
+    api, project_name, old_revision, new_revision):
+  proj_dir = api.path['start_dir'].join('project')
+
+  if api.roll_util.check_roll_direction(api.path['checkout'], old_revision,
+                                        new_revision):
+    api.roll_util.message(project_name, proj_dir, old_revision, new_revision)
+  else:
+    api.roll_util.backwards_roll_step('remote', old_revision, new_revision)
+
+def GenTests(api):  # pylint: disable=invalid-name
+  yield (
+      api.status_check.test('singlecommit_singleline')
+      + api.properties(
+          project_name='proj',
+          old_revision='0'*40,
+          new_revision='1'*40)
+      + api.roll_util.commit_data(
+          api.roll_util.commit('1'*40, 'foo\n\nbar'))
+      + api.roll_util.rolls_forward()
+      + api.roll_util.not_cancelled()
+  )
+
+  yield (
+      api.status_check.test('singlecommit_multiline')
+      + api.properties(
+          project_name='proj',
+          old_revision='0'*40,
+          new_revision='1'*40)
+      + api.roll_util.commit_data(
+          api.roll_util.commit('1'*40, 'foo\n\nbar\n\nBug: 123\nCC: foo'))
+      + api.roll_util.rolls_forward()
+      + api.roll_util.not_cancelled()
+  )
+
+  yield (
+      api.status_check.test('multicommit')
+      + api.properties(
+          project_name='proj',
+          old_revision='0'*40,
+          new_revision='4'*40)
+      + api.roll_util.commit_data(
+          api.roll_util.commit('4'*40, 'xyz\n\nxyz'),
+          api.roll_util.commit('3'*40, 'baz\n\nbaz'),
+          api.roll_util.commit('2'*40,
+                                  ''.join(x * 10 for x in '0123456789')),
+          api.roll_util.commit('1'*40, 'foo\n\nfoo'),
+      )
+      + api.roll_util.rolls_forward()
+      + api.roll_util.not_cancelled()
+  )
+
+  yield (
+      api.status_check.test('frombranch')
+      + api.properties(
+          project_name='proj',
+          old_revision='main',
+          new_revision='5'*40)
+      + api.roll_util.commit_data(
+          api.roll_util.commit('5'*40, 'five'),
+          api.roll_util.commit('4'*40, 'four'),
+          api.roll_util.commit('3'*40, 'three'),
+          api.roll_util.commit('2'*40, 'two'),
+          api.roll_util.commit('1'*40, 'one'),
+      )
+      + api.roll_util.rolls_forward()
+      + api.roll_util.not_cancelled()
+  )
+
+  yield (
+      api.status_check.test('backwards')
+      + api.properties(
+          project_name='proj',
+          old_revision='0'*40,
+          new_revision='1'*40)
+      + api.roll_util.rolls_backward()
+      + api.roll_util.cancelled()
+  )
+
+  yield (
+      api.status_check.test('up_to_date')
+      + api.properties(
+          project_name='proj',
+          old_revision='1'*40,
+          new_revision='1'*40)
+      + api.roll_util.cancelled()
+  )
diff --git a/recipes/repo_roller.expected/backwards.json b/recipes/repo_roller.expected/backwards.json
new file mode 100644
index 0000000..3f71956
--- /dev/null
+++ b/recipes/repo_roller.expected/backwards.json
@@ -0,0 +1,787 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout manifest"
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.change data",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.change data.process gitiles commit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.change data.process gitiles commit.install infra/tools/luci/gerrit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest"
+    ],
+    "infra_step": true,
+    "name": "checkout manifest.change data.process gitiles commit.install infra/tools/luci/gerrit.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest",
+      "-ensure-file",
+      "infra/tools/luci/gerrit/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "checkout manifest.change data.process gitiles commit.install infra/tools/luci/gerrit.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/gerrit/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest/gerrit",
+      "change-query",
+      "-host",
+      "https://foo-review.googlesource.com",
+      "-input",
+      "{\"params\": {\"q\": \"commit:2d72510e447ab60a9728aeea2362d8be2cbd7789\"}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "checkout manifest.change data.process gitiles commit.number",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"_number\": \"1234\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"master\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.change data.changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.change data.changes.foo:1234",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@_Change(number='1234', remote='https://foo.googlesource.com/a', ref=u'2d72510e447ab60a9728aeea2362d8be2cbd7789', rebase=False, branch='master', gerrit_name=u'foo')@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/checkout"
+    ],
+    "infra_step": true,
+    "name": "checkout manifest.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "remote",
+      "add",
+      "origin",
+      "https://host.googlesource.com/manifest"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.git remote",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.cache",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/git/host.googlesource.com-manifest"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.cache.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init",
+      "--bare"
+    ],
+    "cwd": "[CACHE]/git/host.googlesource.com-manifest",
+    "infra_step": true,
+    "name": "checkout manifest.cache.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "remote.origin.url",
+      "https://host.googlesource.com/manifest"
+    ],
+    "cwd": "[CACHE]/git/host.googlesource.com-manifest",
+    "infra_step": true,
+    "name": "checkout manifest.cache.remote set-url",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "--replace-all",
+      "remote.origin.fetch",
+      "+refs/heads/*:refs/heads/*",
+      "\\+refs/heads/\\*:.*"
+    ],
+    "cwd": "[CACHE]/git/host.googlesource.com-manifest",
+    "infra_step": true,
+    "name": "checkout manifest.cache.git config",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--prune",
+      "--tags",
+      "origin"
+    ],
+    "cwd": "[CACHE]/git/host.googlesource.com-manifest",
+    "infra_step": true,
+    "name": "checkout manifest.cache.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/checkout/.git/objects/info"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.cache.makedirs object/info",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CACHE]/git/host.googlesource.com-manifest/objects\n",
+      "[START_DIR]/checkout/.git/objects/info/alternates"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.cache.alternates",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@alternates@[CACHE]/git/host.googlesource.com-manifest/objects@@@",
+      "@@@STEP_LOG_END@alternates@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--tags",
+      "origin",
+      "master",
+      "--recurse-submodules"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.git rev-parse",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.submodule",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.submodule.git submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout manifest.submodule.git submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout manifest.git log",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "log",
+      "--oneline",
+      "-n",
+      "10"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "name": "checkout manifest.git log.[START_DIR]/checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[START_DIR]/checkout/default.xml",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read manifest",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@default.xml@<?xml version=\"1.0\" encoding=\"UTF-8\"?>@@@",
+      "@@@STEP_LOG_LINE@default.xml@<manifest>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@</manifest>@@@",
+      "@@@STEP_LOG_END@default.xml@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a"
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.change data",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.change data.process gitiles commit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest/gerrit",
+      "change-query",
+      "-host",
+      "https://foo-review.googlesource.com",
+      "-input",
+      "{\"params\": {\"q\": \"commit:2d72510e447ab60a9728aeea2362d8be2cbd7789\"}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "checkout a.change data.process gitiles commit.number",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"_number\": \"1234\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"master\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.change data.changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.change data.changes.foo:1234",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@_Change(number='1234', remote='https://foo.googlesource.com/a', ref=u'2d72510e447ab60a9728aeea2362d8be2cbd7789', rebase=False, branch='master', gerrit_name=u'foo')@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/project"
+    ],
+    "infra_step": true,
+    "name": "checkout a.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "remote",
+      "add",
+      "origin",
+      "https://foo.googlesource.com/a"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.git remote",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.cache",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/git/foo.googlesource.com-a"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.cache.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init",
+      "--bare"
+    ],
+    "cwd": "[CACHE]/git/foo.googlesource.com-a",
+    "infra_step": true,
+    "name": "checkout a.cache.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "remote.origin.url",
+      "https://foo.googlesource.com/a"
+    ],
+    "cwd": "[CACHE]/git/foo.googlesource.com-a",
+    "infra_step": true,
+    "name": "checkout a.cache.remote set-url",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "--replace-all",
+      "remote.origin.fetch",
+      "+refs/heads/*:refs/heads/*",
+      "\\+refs/heads/\\*:.*"
+    ],
+    "cwd": "[CACHE]/git/foo.googlesource.com-a",
+    "infra_step": true,
+    "name": "checkout a.cache.git config",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--prune",
+      "--tags",
+      "origin"
+    ],
+    "cwd": "[CACHE]/git/foo.googlesource.com-a",
+    "infra_step": true,
+    "name": "checkout a.cache.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/project/.git/objects/info"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.cache.makedirs object/info",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CACHE]/git/foo.googlesource.com-a/objects\n",
+      "[START_DIR]/project/.git/objects/info/alternates"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.cache.alternates",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@alternates@[CACHE]/git/foo.googlesource.com-a/objects@@@",
+      "@@@STEP_LOG_END@alternates@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--tags",
+      "origin",
+      "master",
+      "--recurse-submodules"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.git rev-parse",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.submodule",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.submodule.git submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/project",
+    "infra_step": true,
+    "name": "checkout a.submodule.git submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout a.git log",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "log",
+      "--oneline",
+      "-n",
+      "10"
+    ],
+    "cwd": "[START_DIR]/project",
+    "name": "checkout a.git log.[START_DIR]/project",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[START_DIR]/project",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@backward@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "cancelling roll",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@not updating from 1111111 to 2d72510 because 1111111 is newer than 2d72510@@@",
+      "@@@STEP_LINK@1111111111111111111111111111111111111111@https://foo.googlesource.com/a/+/1111111111111111111111111111111111111111@@@",
+      "@@@STEP_LINK@2d72510e447ab60a9728aeea2362d8be2cbd7789@https://foo.googlesource.com/a/+/2d72510e447ab60a9728aeea2362d8be2cbd7789@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/repo_roller.expected/name-not-found.json b/recipes/repo_roller.expected/name-not-found.json
index 3fe6c67..e4368c4 100644
--- a/recipes/repo_roller.expected/name-not-found.json
+++ b/recipes/repo_roller.expected/name-not-found.json
@@ -411,8 +411,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
diff --git a/recipes/repo_roller.expected/no-trigger-with-revision-branch.json b/recipes/repo_roller.expected/no-trigger-with-revision-branch.json
index 0303c97..7ee7ca9 100644
--- a/recipes/repo_roller.expected/no-trigger-with-revision-branch.json
+++ b/recipes/repo_roller.expected/no-trigger-with-revision-branch.json
@@ -318,8 +318,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
@@ -645,7 +645,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"hash-from-special-checkout\" upstream=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"hash-from-special-checkout\" upstream=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
       "[START_DIR]/checkout/default.xml"
     ],
     "infra_step": true,
@@ -656,8 +656,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"hash-from-special-checkout\" upstream=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\" />@@@",
diff --git a/recipes/repo_roller.expected/no-trigger-with-revision-hash.json b/recipes/repo_roller.expected/no-trigger-with-revision-hash.json
index aa5f614..79224ef 100644
--- a/recipes/repo_roller.expected/no-trigger-with-revision-hash.json
+++ b/recipes/repo_roller.expected/no-trigger-with-revision-hash.json
@@ -318,8 +318,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
diff --git a/recipes/repo_roller.expected/no-trigger-with-revision-tag.json b/recipes/repo_roller.expected/no-trigger-with-revision-tag.json
index 3a4afe9..26c401c 100644
--- a/recipes/repo_roller.expected/no-trigger-with-revision-tag.json
+++ b/recipes/repo_roller.expected/no-trigger-with-revision-tag.json
@@ -318,8 +318,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
diff --git a/recipes/repo_roller.expected/no-trigger-with-upstream.json b/recipes/repo_roller.expected/no-trigger-with-upstream.json
index 3863392..f91ce62 100644
--- a/recipes/repo_roller.expected/no-trigger-with-upstream.json
+++ b/recipes/repo_roller.expected/no-trigger-with-upstream.json
@@ -318,8 +318,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
@@ -639,13 +639,27 @@
   },
   {
     "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "hash-from-special-checkout"
+    ],
+    "cwd": "[START_DIR]/project",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"hash-from-special-checkout\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"hash-from-special-checkout\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
       "[START_DIR]/checkout/default.xml"
     ],
     "infra_step": true,
@@ -657,7 +671,7 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"hash-from-special-checkout\" upstream=\"master\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\" />@@@",
@@ -673,7 +687,7 @@
     "cmd": [
       "git",
       "log",
-      "hash-from-special-checkout~5..hash-from-special-checkout",
+      "1111111111111111111111111111111111111111..hash-from-special-checkout",
       "--pretty=format:%H %B",
       "-z"
     ],
@@ -694,7 +708,7 @@
       "@@@STEP_LOG_LINE@template@CQ-Do-Not-Cancel-Tryjobs: true@@@",
       "@@@STEP_LOG_END@template@@@",
       "@@@STEP_LOG_LINE@kwargs@{'new_revision': 'hash-from-special-checkout',@@@",
-      "@@@STEP_LOG_LINE@kwargs@ 'old_revision': '1',@@@",
+      "@@@STEP_LOG_LINE@kwargs@ 'old_revision': '1111111111111111111111111111111111111111',@@@",
       "@@@STEP_LOG_LINE@kwargs@ 'original_message': 'foo\\nbar',@@@",
       "@@@STEP_LOG_LINE@kwargs@ 'project_name': 'a1',@@@",
       "@@@STEP_LOG_LINE@kwargs@ 'sanitized_message': 'foo\\nbar'}@@@",
@@ -702,7 +716,7 @@
       "@@@STEP_LOG_LINE@message@[roll a1] foo@@@",
       "@@@STEP_LOG_LINE@message@bar@@@",
       "@@@STEP_LOG_LINE@message@@@@",
-      "@@@STEP_LOG_LINE@message@Rolled-Commits: 1..hash-from-speci@@@",
+      "@@@STEP_LOG_LINE@message@Rolled-Commits: 111111111111111..hash-from-speci@@@",
       "@@@STEP_LOG_LINE@message@CQ-Do-Not-Cancel-Tryjobs: true@@@",
       "@@@STEP_LOG_END@message@@@"
     ]
@@ -819,7 +833,7 @@
       "git",
       "commit",
       "-m",
-      "[roll a1] foo\nbar\n\nRolled-Commits: 1..hash-from-speci\nCQ-Do-Not-Cancel-Tryjobs: true\nChange-Id: Iabc1abc1abc1abc1abc1abc1abc1abc1abc1abc1\n",
+      "[roll a1] foo\nbar\n\nRolled-Commits: 111111111111111..hash-from-speci\nCQ-Do-Not-Cancel-Tryjobs: true\nChange-Id: Iabc1abc1abc1abc1abc1abc1abc1abc1abc1abc1\n",
       "-a"
     ],
     "cwd": "[START_DIR]/checkout",
diff --git a/recipes/repo_roller.expected/success.json b/recipes/repo_roller.expected/success.json
index 1f7a8ec..c4a56db 100644
--- a/recipes/repo_roller.expected/success.json
+++ b/recipes/repo_roller.expected/success.json
@@ -411,8 +411,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
@@ -760,13 +760,27 @@
   },
   {
     "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[START_DIR]/project",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"2d72510e447ab60a9728aeea2362d8be2cbd7789\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"2d72510e447ab60a9728aeea2362d8be2cbd7789\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
       "[START_DIR]/checkout/default.xml"
     ],
     "infra_step": true,
@@ -778,7 +792,7 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"2d72510e447ab60a9728aeea2362d8be2cbd7789\" upstream=\"master\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\" />@@@",
@@ -794,7 +808,7 @@
     "cmd": [
       "git",
       "log",
-      "2d72510e447ab60a9728aeea2362d8be2cbd7789~5..2d72510e447ab60a9728aeea2362d8be2cbd7789",
+      "1111111111111111111111111111111111111111..2d72510e447ab60a9728aeea2362d8be2cbd7789",
       "--pretty=format:%H %B",
       "-z"
     ],
@@ -815,7 +829,7 @@
       "@@@STEP_LOG_LINE@template@CQ-Do-Not-Cancel-Tryjobs: true@@@",
       "@@@STEP_LOG_END@template@@@",
       "@@@STEP_LOG_LINE@kwargs@{'new_revision': u'2d72510e447ab60a9728aeea2362d8be2cbd7789',@@@",
-      "@@@STEP_LOG_LINE@kwargs@ 'old_revision': '1',@@@",
+      "@@@STEP_LOG_LINE@kwargs@ 'old_revision': '1111111111111111111111111111111111111111',@@@",
       "@@@STEP_LOG_LINE@kwargs@ 'original_message': 'foo\\nbar',@@@",
       "@@@STEP_LOG_LINE@kwargs@ 'project_name': 'a1',@@@",
       "@@@STEP_LOG_LINE@kwargs@ 'sanitized_message': 'foo\\nbar'}@@@",
@@ -823,7 +837,7 @@
       "@@@STEP_LOG_LINE@message@[roll a1] foo@@@",
       "@@@STEP_LOG_LINE@message@bar@@@",
       "@@@STEP_LOG_LINE@message@@@@",
-      "@@@STEP_LOG_LINE@message@Rolled-Commits: 1..2d72510e447ab60@@@",
+      "@@@STEP_LOG_LINE@message@Rolled-Commits: 111111111111111..2d72510e447ab60@@@",
       "@@@STEP_LOG_LINE@message@CQ-Do-Not-Cancel-Tryjobs: true@@@",
       "@@@STEP_LOG_END@message@@@"
     ]
@@ -886,7 +900,7 @@
       "git",
       "commit",
       "-m",
-      "[roll a1] foo\nbar\n\nRolled-Commits: 1..2d72510e447ab60\nCQ-Do-Not-Cancel-Tryjobs: true\nChange-Id: Iabc1abc1abc1abc1abc1abc1abc1abc1abc1abc1\n",
+      "[roll a1] foo\nbar\n\nRolled-Commits: 111111111111111..2d72510e447ab60\nCQ-Do-Not-Cancel-Tryjobs: true\nChange-Id: Iabc1abc1abc1abc1abc1abc1abc1abc1abc1abc1\n",
       "-a"
     ],
     "cwd": "[START_DIR]/checkout",
diff --git a/recipes/repo_roller.expected/upstream-not-set-revision-not-branch.json b/recipes/repo_roller.expected/upstream-not-set-revision-not-branch.json
index 5859e33..6eade48 100644
--- a/recipes/repo_roller.expected/upstream-not-set-revision-not-branch.json
+++ b/recipes/repo_roller.expected/upstream-not-set-revision-not-branch.json
@@ -411,8 +411,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
diff --git a/recipes/repo_roller.expected/upstream-not-set.json b/recipes/repo_roller.expected/upstream-not-set.json
index 54e9551..b5a6207 100644
--- a/recipes/repo_roller.expected/upstream-not-set.json
+++ b/recipes/repo_roller.expected/upstream-not-set.json
@@ -411,8 +411,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"foo\" fetch=\"sso://foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote name=\"bar\" fetch=\"sso://bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\"/>@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\"/>@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"master\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\"/>@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\"/>@@@",
@@ -766,7 +766,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"2d72510e447ab60a9728aeea2362d8be2cbd7789\" upstream=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<manifest>\n  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />\n  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />\n  <default remote=\"bar\" />\n  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\" />\n  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />\n  <project name=\"c\" path=\"c3\" revision=\"2d72510e447ab60a9728aeea2362d8be2cbd7789\" upstream=\"master\" />\n  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />\n  <project name=\"e5\" revision=\"refs/tags/e\" />\n</manifest>\n",
       "[START_DIR]/checkout/default.xml"
     ],
     "infra_step": true,
@@ -777,8 +777,8 @@
       "@@@STEP_LOG_LINE@default.xml@  <remote fetch=\"sso://foo\" name=\"foo\" review=\"sso://foo-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <remote fetch=\"sso://bar\" name=\"bar\" review=\"sso://bar-review\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <default remote=\"bar\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1\" upstream=\"master\" />@@@",
-      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2\" upstream=\"master\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"a\" path=\"a1\" remote=\"foo\" revision=\"1111111111111111111111111111111111111111\" upstream=\"master\" />@@@",
+      "@@@STEP_LOG_LINE@default.xml@  <project name=\"b\" path=\"b2\" revision=\"2222222222222222222222222222222222222222\" upstream=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"c\" path=\"c3\" revision=\"2d72510e447ab60a9728aeea2362d8be2cbd7789\" upstream=\"master\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"d\" path=\"d4\" revision=\"0000000000111111111122222222223333333333\" />@@@",
       "@@@STEP_LOG_LINE@default.xml@  <project name=\"e5\" revision=\"refs/tags/e\" />@@@",
diff --git a/recipes/repo_roller.py b/recipes/repo_roller.py
index 7534509..18599fc 100644
--- a/recipes/repo_roller.py
+++ b/recipes/repo_roller.py
@@ -24,7 +24,7 @@
     'fuchsia/sso',
     'fuchsia/status_check',
     'pigweed/checkout',
-    'pigweed/roll_message',
+    'pigweed/roll_util',
     'recipe_engine/buildbucket',
     'recipe_engine/cipd',
     'recipe_engine/file',
@@ -174,6 +174,13 @@
   # Explicitly update both proj.attrib and proj_attrib to minimize confusion.
   proj_attrib['revision'] = proj.attrib['revision'] = new_revision
 
+  if (not _is_branch(old_revision) and
+      not api.roll_util.check_roll_direction(proj_dir, old_revision,
+                                             new_revision)):
+    api.roll_util.backwards_roll_step(manifest_remote, old_revision,
+                                      new_revision)
+    return
+
   doc = xml.etree.cElementTree.tostring(root)
 
   api.file.write_text('write manifest', filepath,
@@ -184,7 +191,7 @@
       gerrit_project=api.checkout.gerrit_project(),
       upstream_ref=api.checkout.branch,
       repo_dir=api.checkout.root,
-      commit_message=api.roll_message(
+      commit_message=api.roll_util.message(
           project_name=path_to_update,
           old_revision=old_revision,
           new_revision=new_revision,
@@ -215,16 +222,16 @@
   <remote name="foo" fetch="sso://foo" review="sso://foo-review" />
   <remote name="bar" fetch="sso://bar" review="sso://bar-review" />
   <default remote="bar" />
-  <project name="a" path="a1" remote="foo" revision="1" upstream="master"/>
-  <project name="b" path="b2" revision="2" upstream="master"/>
+  <project name="a" path="a1" remote="foo" revision="1111111111111111111111111111111111111111" upstream="master"/>
+  <project name="b" path="b2" revision="2222222222222222222222222222222222222222" upstream="master"/>
   <project name="c" path="c3" revision="master"/>
   <project name="d" path="d4" revision="0000000000111111111122222222223333333333"/>
   <project name="e5" revision="refs/tags/e"/>
 </manifest>
 """.lstrip()))
 
-  commit_data = api.roll_message.commit_data(
-      api.roll_message.commit('a'*40, 'foo\nbar'))
+  commit_data = api.roll_util.commit_data(
+      api.roll_util.commit('a'*40, 'foo\nbar'))
 
   yield (
       api.status_check.test('success')
@@ -232,6 +239,7 @@
       + api.checkout.ci_test_data(git_repo='https://foo.googlesource.com/a')
       + commit_data
       + read_step_data()
+      + api.roll_util.rolls_forward()
       + api.auto_roller.dry_run_success()
   )  # yapf: disable
 
@@ -295,3 +303,11 @@
       + api.step_data('git log',
                       stdout=api.raw_io.output('hash-from-special-checkout'))
   )  # yapf: disable
+
+  yield (
+      api.status_check.test('backwards')
+      + properties(api, path='a1')
+      + api.checkout.ci_test_data(git_repo='https://foo.googlesource.com/a')
+      + read_step_data()
+      + api.roll_util.rolls_backward()
+  )  # yapf: disable
diff --git a/recipes/submodule_roller.expected/backwards.json b/recipes/submodule_roller.expected/backwards.json
new file mode 100644
index 0000000..bbd207f
--- /dev/null
+++ b/recipes/submodule_roller.expected/backwards.json
@@ -0,0 +1,469 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout pigweed"
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gitiles commit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gitiles commit.install infra/tools/luci/gerrit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest"
+    ],
+    "infra_step": true,
+    "name": "checkout pigweed.change data.process gitiles commit.install infra/tools/luci/gerrit.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest",
+      "-ensure-file",
+      "infra/tools/luci/gerrit/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "checkout pigweed.change data.process gitiles commit.install infra/tools/luci/gerrit.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/gerrit/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CACHE]/cipd/infra/tools/luci/gerrit/latest/gerrit",
+      "change-query",
+      "-host",
+      "https://foo-review.googlesource.com",
+      "-input",
+      "{\"params\": {\"q\": \"commit:2d72510e447ab60a9728aeea2362d8be2cbd7789\"}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "name": "checkout pigweed.change data.process gitiles commit.number",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@json.output@[@@@",
+      "@@@STEP_LOG_LINE@json.output@  {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"_number\": \"1234\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"master\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@]@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes.foo:1234",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@_Change(number='1234', remote='https://foo.googlesource.com/a1', ref=u'2d72510e447ab60a9728aeea2362d8be2cbd7789', rebase=False, branch='master', gerrit_name=u'foo')@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/checkout"
+    ],
+    "infra_step": true,
+    "name": "checkout pigweed.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "remote",
+      "add",
+      "origin",
+      "https://pigweed.googlesource.com/pigweed/pigweed"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.git remote",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.cache",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.makedirs",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "init",
+      "--bare"
+    ],
+    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.git init",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "remote.origin.url",
+      "https://pigweed.googlesource.com/pigweed/pigweed"
+    ],
+    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.remote set-url",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "config",
+      "--replace-all",
+      "remote.origin.fetch",
+      "+refs/heads/*:refs/heads/*",
+      "\\+refs/heads/\\*:.*"
+    ],
+    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.git config",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--prune",
+      "--tags",
+      "origin"
+    ],
+    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/checkout/.git/objects/info"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.makedirs object/info",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed/objects\n",
+      "[START_DIR]/checkout/.git/objects/info/alternates"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.cache.alternates",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@alternates@[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed/objects@@@",
+      "@@@STEP_LOG_END@alternates@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "--tags",
+      "origin",
+      "master",
+      "--recurse-submodules"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.git rev-parse",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.submodule",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.submodule.git submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "infra_step": true,
+    "name": "checkout pigweed.submodule.git submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.git log",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "log",
+      "--oneline",
+      "-n",
+      "10"
+    ],
+    "cwd": "[START_DIR]/checkout",
+    "name": "checkout pigweed.git log.[START_DIR]/checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[START_DIR]/checkout/.gitmodules",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read .gitmodules",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@.gitmodules@[submodule \"a1\"]@@@",
+      "@@@STEP_LOG_LINE@.gitmodules@\tpath = a1@@@",
+      "@@@STEP_LOG_LINE@.gitmodules@\turl = https://foo.googlesource.com/a1@@@",
+      "@@@STEP_LOG_END@.gitmodules@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "get old revision"
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "origin",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "git fetch"
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "git checkout"
+  },
+  {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@backward@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "cancelling roll",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@not updating from 1111111 to 2d72510 because 1111111 is newer than 2d72510@@@",
+      "@@@STEP_LINK@1111111111111111111111111111111111111111@https://foo.googlesource.com/a1/+/1111111111111111111111111111111111111111@@@",
+      "@@@STEP_LINK@2d72510e447ab60a9728aeea2362d8be2cbd7789@https://foo.googlesource.com/a1/+/2d72510e447ab60a9728aeea2362d8be2cbd7789@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/submodule_roller.expected/success-sso.json b/recipes/submodule_roller.expected/success-sso.json
index fad2fef..9729c96 100644
--- a/recipes/submodule_roller.expected/success-sso.json
+++ b/recipes/submodule_roller.expected/success-sso.json
@@ -441,6 +441,20 @@
     "name": "git checkout"
   },
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "2d72510e447ab60a9728aeea2362d8be2cbd7789"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipes/submodule_roller.expected/with-branch-prop.json b/recipes/submodule_roller.expected/with-branch-prop.json
index 684bc60..88643c0 100644
--- a/recipes/submodule_roller.expected/with-branch-prop.json
+++ b/recipes/submodule_roller.expected/with-branch-prop.json
@@ -357,6 +357,20 @@
     "name": "get new revision"
   },
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "2222222222222222222222222222222222222222"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipes/submodule_roller.expected/with-newrev-prop.json b/recipes/submodule_roller.expected/with-newrev-prop.json
index 21ead8b..da866bc 100644
--- a/recipes/submodule_roller.expected/with-newrev-prop.json
+++ b/recipes/submodule_roller.expected/with-newrev-prop.json
@@ -348,6 +348,20 @@
     "name": "git checkout"
   },
   {
+    "cmd": [
+      "git",
+      "merge-base",
+      "--is-ancestor",
+      "1111111111111111111111111111111111111111",
+      "cccccccccccccccccccccccccccccccccccccccc"
+    ],
+    "cwd": "[START_DIR]/checkout/a1",
+    "name": "check roll direction",
+    "~followup_annotations": [
+      "@@@STEP_SUMMARY_TEXT@forward@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "roll message"
   },
diff --git a/recipes/submodule_roller.py b/recipes/submodule_roller.py
index 2add60e..6fe0d2b 100644
--- a/recipes/submodule_roller.py
+++ b/recipes/submodule_roller.py
@@ -26,7 +26,7 @@
     'fuchsia/sso',
     'fuchsia/status_check',
     'pigweed/checkout',
-    'pigweed/roll_message',
+    'pigweed/roll_util',
     'recipe_engine/buildbucket',
     'recipe_engine/cipd',
     'recipe_engine/context',
@@ -147,12 +147,17 @@
     if not re.search(r'^[0-9a-f]{40}$', new_revision):
       new_revision = get_revision('get new revision', '2' * 40)
 
+  if not api.roll_util.check_roll_direction(submodule_dir, old_revision,
+                                            new_revision):
+    api.roll_util.backwards_roll_step(remote, old_revision, new_revision)
+    return
+
   change = api.auto_roller.attempt_roll(
       gerrit_host=api.checkout.gerrit_host(),
       gerrit_project=api.checkout.gerrit_project(),
       upstream_ref=api.checkout.branch,
       repo_dir=api.checkout.root,
-      commit_message=api.roll_message(
+      commit_message=api.roll_util.message(
           project_name=submodule_path,
           old_revision=old_revision,
           new_revision=new_revision,
@@ -183,8 +188,8 @@
           k, _url(v)))
     return api.step_data('read .gitmodules', api.file.read_text(''.join(text)))
 
-  commit_data = api.roll_message.commit_data(
-      api.roll_message.commit('a'*40, 'foo\nbar'))
+  commit_data = api.roll_util.commit_data(
+      api.roll_util.commit('a'*40, 'foo\nbar'))
 
   yield (
       api.status_check.test('success-sso')
@@ -192,6 +197,7 @@
       + trigger('a1')
       + commit_data
       + gitmodules(a1='sso://foo/a1')
+      + api.roll_util.rolls_forward()
       + api.auto_roller.dry_run_success()
   )  # yapf: disable
 
@@ -214,6 +220,7 @@
       + api.properties(submodule_path='a1', new_revision='c'*40)
       + commit_data
       + gitmodules(a1='a1')
+      + api.roll_util.rolls_forward()
       + api.auto_roller.dry_run_success()
   )  # yapf: disable
 
@@ -222,6 +229,7 @@
       + api.properties(submodule_path='a1', submodule_branch='branch')
       + commit_data
       + gitmodules(a1='a1')
+      + api.roll_util.rolls_forward()
       + api.auto_roller.dry_run_success()
   )  # yapf: disable
 
@@ -229,3 +237,11 @@
       api.status_check.test('no-revision', status='failure')
       + api.properties(submodule_path='a1')
   )  # yapf: disable
+
+  yield (
+      api.status_check.test('backwards')
+      + api.properties(submodule_path='a1')
+      + trigger('a1')
+      + gitmodules(a1='a1')
+      + api.roll_util.rolls_backward()
+  )  # yapf: disable