autotest_quickmerge no-ops if sysroot is already newer than source

With this CL, autotest_quickmerge compares the modification time of the
newest file in the source tree with the sysroot tree. If the sysroot
tree is newer, this most likely indicates that the user has already
either quickmerged or emerged their changes, in which case it is better
for autotest_quickmerge to do nothing (*especially* in the latter
already-emerged case, since the sysroot is not only up to date with
changes but also a in a more correct state than quickmerge would put it
in).

With this CL, autotest_quickmerge will also drop a .quickmerge_sentinel
file into the sysroot autotest. This file will not exist in the sysroot
after an emerge, so it is an indicator of whether the a quickmerge or
emerge was more recently performed. The modification timestamp of the
sentinel file will match the time at which the last quickmerge was
performed.

CQ-DEPEND=CL:Ib6269894d829b8d4837d5aa15a3de8202c887973
BUG=chromium:248545
TEST=In chroot, ran build_packages. Then ran autotest_quickmerge, and
saw that quickmerge was skipped due to sysroot being newer. Made an edit
to a file in the autotest source repo, ran autotest_quickmerge again,
and saw that quickmerge took place.

Change-Id: I75d4cd058282c3ecee279849da53cdcafade2cf1
Reviewed-on: https://gerrit.chromium.org/gerrit/58732
Tested-by: Aviv Keshet <akeshet@chromium.org>
Reviewed-by: Alex Miller <milleral@chromium.org>
Commit-Queue: Aviv Keshet <akeshet@chromium.org>
diff --git a/scripts/autotest_quickmerge.py b/scripts/autotest_quickmerge.py
index a3dc5dd..44d254f 100644
--- a/scripts/autotest_quickmerge.py
+++ b/scripts/autotest_quickmerge.py
@@ -39,6 +39,11 @@
                      'chromeos-base/autotest-tests-ltp',
                      'chromeos-base/autotest-tests-ownershipapi']
 
+IGNORE_SUBDIRS = ['ExternalSource',
+                  'logs',
+                  'results',
+                  'site-packages']
+
 # Data structure describing a single rsync filesystem change.
 #
 # change_description: An 11 character string, the rsync change description
@@ -61,6 +66,37 @@
   """Exception thrown when unable to retrieve a portage package API."""
 
 
+
+def GetNewestFileTime(path, ignore_subdirs=[]):
+  #pylint: disable-msg=W0102
+  """Recursively determine the newest file modification time.
+
+  Arguments:
+    path: The absolute path of the directory to recursively search.
+    ignore_subdirs: list of names of subdirectores of given path, to be
+                    ignored by recursive search. Useful as a speed
+                    optimization, to ignore directories full of many
+                    files.
+
+  Returns:
+    The modification time of the most recently modified file recursively
+    contained within the specified directory. Returned as seconds since
+    Jan. 1, 1970, 00:00 GMT, with fractional part (floating point number).
+  """
+  command = ['find', path]
+  for ignore in ignore_subdirs:
+    command.extend(['-path', os.path.join(path, ignore), '-prune', '-o'])
+  command.extend(['-printf', r'%T@\n'])
+
+  command_result = cros_build_lib.RunCommandCaptureOutput(command,
+                                                          error_code_ok=True)
+  float_times = [float(str_time) for str_time in
+                command_result.output.split('\n')
+                if str_time != '']
+
+  return max(float_times)
+
+
 def GetStalePackageNames(change_list, autotest_sysroot):
   """Given a rsync change report, returns the names of stale test packages.
 
@@ -293,6 +329,9 @@
                       help='Dry run only, do not modify sysroot autotest.')
   parser.add_argument('--overwrite', action='store_true',
                       help='Overwrite existing files even if newer.')
+  parser.add_argument('--force', action='store_true',
+                      help='Do not check whether destination tree is newer '
+                      'than source tree, always perform quickmerge.')
   parser.add_argument('--verbose', action='store_true',
                       help='Print detailed change report.')
 
@@ -327,6 +366,14 @@
   sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
                                        'autotest', '')
 
+  if not args.force:
+    newest_dest_time = GetNewestFileTime(sysroot_autotest_path, IGNORE_SUBDIRS)
+    newest_source_time = GetNewestFileTime(source_path, IGNORE_SUBDIRS)
+    if newest_dest_time >= newest_source_time:
+      logging.info('The sysroot appears to be newer than the source tree, '
+                   'doing nothing and exiting now.')
+      return 0
+
   rsync_output = RsyncQuickmerge(source_path, sysroot_autotest_path,
                                  include_pattern_file, args.pretend,
                                  args.overwrite)
@@ -338,6 +385,7 @@
                                                 sysroot_autotest_path)
 
   if not args.pretend:
+    logging.info('Updating portage database.')
     UpdatePackageContents(change_report, AUTOTEST_EBUILD,
                           sysroot_path)
     for ebuild in DOWNGRADE_EBUILDS:
@@ -346,6 +394,10 @@
                         ebuild)
     RemoveBzipPackages(sysroot_autotest_path)
 
+    sentinel_filename = os.path.join(sysroot_autotest_path,
+                                     '.quickmerge_sentinel')
+    cros_build_lib.RunCommand(['touch', sentinel_filename])
+
   if args.pretend:
     logging.info('The following message is pretend only. No filesystem '
                  'changes made.')