From 49122c83e37bce41c9c6f8416095116344546adb Mon Sep 17 00:00:00 2001
From: Darren Oh <darrenoh@30772.no-reply.drupal.org>
Date: Sun, 21 Apr 2019 12:07:18 -0400
Subject: [PATCH] Issue #1254200 by Darren Oh: Fix dev release time

---
 git_deploy.module | 235 ++++++++++++++++++++++++++++++----------------
 1 file changed, 152 insertions(+), 83 deletions(-)

diff --git a/git_deploy.module b/git_deploy.module
index 1c6a996..731a5bd 100644
--- a/git_deploy.module
+++ b/git_deploy.module
@@ -2,107 +2,176 @@
 
 /**
  * @file
- * Adds project, version and date information to projects checked out of git.
+ * Adds project, version and date information to projects checked out with Git.
  */
 
 use Drupal\Core\Extension\Extension;
+use Drupal\update\UpdateManagerInterface;
+
+/**
+ * Null device.
+ *
+ * If /dev/null does not exist, assume we are on Windows.
+ */
+define('GIT_DEPLOY_ERROR_DUMP', file_exists('/dev/null') ? '/dev/null' : 'nul');
 
 /**
  * Implements hook_system_info_alter().
  */
 function git_deploy_system_info_alter(array &$info, Extension $file, $type) {
-  static $core_info = NULL;
-
-  $is_core = ($file->origin === 'core') && array_key_exists('version', $info)  &&  preg_match('!-dev$!', $info['version']);
-  if ($is_core) {
-    // If a core version check already happened, just reuse that info.
-    if (!empty($core_info)) {
-      $info['version'] = $core_info['version'];
-      $info['datestamp'] = $core_info['datestamp'];
-      return;
-    }
-    $core_info = array();
-    // Make sure the version checking happens for checkouts posing as
-    // development versions.
-    unset($info['version']);
-  }
-
-  if (empty($info['version'])) {
-    $directory = $file->subpath;
-    while ($directory && !file_exists("$directory/.git")) {
-      $directory = substr($directory, 0,  strrpos($directory, '/'));
-    }
-    $git_dir = DRUPAL_ROOT . (empty($directory) ? '' : '/') . "$directory/.git";
-    if (file_exists($git_dir)) {
-      $git = "git --git-dir $git_dir";
-      // Find first the project name based on fetch URL.
-      // Eat error messages. >& is valid on Windows, too. Also, $output does
-      // not need initialization because it's taken by reference.
-      exec("$git config --get remote.origin.url 2>&1", $output);
-      if (!empty($output)) {
-        $fetch_url = current($output);
-        $project_name = substr($fetch_url, strrpos($fetch_url, '/') + 1);
-        if (substr($project_name, -4) == '.git') {
-          $project_name = substr($project_name, 0, -4);
+  static $projects = [];
+  if ($file->origin == 'core' || empty($info['version'])) {
+    // Make sure Git operates in the right directory.
+    chdir($file->getPath());
+    // Verify that we are in a Git repository.
+    $directory = exec('git rev-parse --show-toplevel 2> ' . GIT_DEPLOY_ERROR_DUMP);
+    if (!empty($directory)) {
+      // Only check Git once per repository.
+      if (!isset($projects[$directory])) {
+        $projects[$directory] = [];
+        // Get upstream info.
+        list($core) = explode('.', $info['core']);
+        $upstream = _git_deploy_get_upstream($file->origin == 'core' ? "$core*" : "$info[core]-*");
+        // Find the project name based on fetch URL.
+        if (isset($upstream['remote'])) {
+          $fetch_url = exec("git config --get remote.$upstream[remote].url 2> " . GIT_DEPLOY_ERROR_DUMP);
+          if (!empty($fetch_url)) {
+            $projects[$directory]['project'] = basename($fetch_url, '.git');
+          }
         }
-        $info['project'] = $project_name;
-      }
-      // Try to fill in branch and tag.
-      exec("$git rev-parse --abbrev-ref HEAD 2>&1", $branch);
-      $tag_found = FALSE;
-      if ($branch) {
-        $branch = $branch[0];
-        if ($is_core) {
-          // Core version is 8.{x}.{y}.
-          $branch_preg =  '\d+\.\d+\.';
+        // Set version from tag.
+        if (isset($upstream['tag'])) {
+          $projects[$directory]['version'] = $upstream['tag'];
         }
-        else {
-          // Contrib version is 8.x-{y}...
-          $branch_preg =  '\d+\.x-\d+\.';
+        // If tag was not found, set version from branch.
+        elseif (isset($upstream['branch'])) {
+          $projects[$directory]['version'] = "$upstream[branch]-dev";
         }
-        if (preg_match('/^' . $branch_preg . 'x$/', $branch)) {
-          $info['version'] = $branch . '-dev';
-          // Nail down the core and the major version now that we know
-          // what they are.
-          $branch_preg = preg_quote(substr($branch, 0, -1));
+        // Set project datestamp.
+        if (isset($upstream['datestamp'])) {
+          $projects[$directory]['datestamp'] = $upstream['datestamp'];
         }
-        // Now try to find a tag.
-        exec("$git rev-list --topo-order --max-count=1 HEAD 2>&1", $last_tag_hash);
-        if ($last_tag_hash) {
-          exec("$git describe  --tags $last_tag_hash[0] 2>&1", $last_tag);
-          if ($last_tag) {
-            $last_tag = $last_tag[0];
-            // Make sure the tag starts as Drupal formatted (for eg.
-            // 7.x-1.0-alpha1) and if we are on a proper branch (ie. not
-            // master) then it's on that branch.
-            if (!$is_core) {
-              $tag_preg = '/^(' . $branch_preg . '\d+(?:-[^-]+)?)(-(\d+-)g[0-9a-f]{7})?$/';
-            }
-            else {
-              $tag_preg = '/^(\d\.\d\.\d+(?:-[^-]+)?)(-(\d+-)g[0-9a-f]{7})?$/';
-            }
-            if (preg_match($tag_preg, $last_tag, $matches)) {
-              $tag_found = TRUE;
-              $info['version'] = isset($matches[2]) ? $matches[1] . '+' . $matches[3] . 'dev' : $last_tag;
+      }
+      $info = $projects[$directory] + $info;
+    }
+    // Switch back to Drupal root folder.
+    chdir(DRUPAL_ROOT);
+  }
+}
+
+/**
+ * Gets upstream info.
+ *
+ * This function must be run from within a Git repository.
+ *
+ * @param string $pattern
+ *   Pattern for matching branch names, without trailing ".x". Must not include
+ *   remote. Also used to check for release tags.
+ *
+ * @return string[]
+ *   Array with the following keys, if found:
+ *   - branch: Best matching remote branch.
+ *   - remote: Remote containing best matching branch.
+ *   - tag: Release tag from last common commit in matching branch.
+ *   - datestamp: Unix timestamp of last common commit.
+ */
+function _git_deploy_get_upstream($pattern = '*') {
+  $upstream = [];
+  // Get tracked upstream branch.
+  $remote = exec('git rev-parse --abbrev-ref @{upstream} 2> ' . GIT_DEPLOY_ERROR_DUMP);
+  if (fnmatch("*/$pattern.x", $remote)) {
+    // Set remote.
+    list($upstream['branch'], $upstream['remote']) = array_reverse(explode('/', $remote));
+    // Find last common commit in both local and upstream.
+    $last_base = exec("git merge-base HEAD $remote 2> " . GIT_DEPLOY_ERROR_DUMP);
+    // Get time of last common commit.
+    $upstream['datestamp'] = exec("git log -1 --pretty=format:%at $last_base 2> " . GIT_DEPLOY_ERROR_DUMP);
+  }
+  else {
+    // If local does not track an upstream branch, find best matching remote
+    // branch.
+    exec('git remote 2> ' . GIT_DEPLOY_ERROR_DUMP, $remotes);
+    if (!empty($remotes)) {
+      // Only consider the origin remote, if it exists.
+      $branch_pattern = in_array('origin', $remotes) ? "origin/$pattern.x" : "*/$pattern.x";
+      // List matching branches in descending order.
+      exec("git branch -r --sort=-refname --list --format='%(refname:short)' $branch_pattern 2> " . GIT_DEPLOY_ERROR_DUMP, $branches);
+      if (!empty($branches)) {
+        $remote = array_shift($branches);
+        // Find last common commit in both local and upstream.
+        $last_base = exec("git merge-base HEAD $remote 2> " . GIT_DEPLOY_ERROR_DUMP);
+        if (!empty($branches)) {
+          // There's no reason to check other branches for a more recent common
+          // commit if they do not contain the base commit, so filter the branch
+          // list.
+          exec("git branch -r --sort=-refname --contains $last_base --no-contains $remote --format='%(refname:short)' $branch_pattern 2> " . GIT_DEPLOY_ERROR_DUMP, $branches);
+          while (!empty($branches)) {
+            $branch = array_shift($branches);
+            // Find last common commit in both local and upstream.
+            $base = exec("git merge-base HEAD $branch 2> " . GIT_DEPLOY_ERROR_DUMP);
+            // Count commits since last base commit.
+            $difference = exec("git rev-list --count $base ^$last_base 2> " . GIT_DEPLOY_ERROR_DUMP);
+            // If base contains new commits, it is a better match.
+            if (!empty($difference)) {
+              $remote = $branch;
+              $last_base = $base;
+              if (!empty($branches)) {
+                // Filter the branch list.
+                exec("git branch -r --sort=-refname --contains $last_base --no-contains $remote --format='%(refname:short)' $branch_pattern 2> " . GIT_DEPLOY_ERROR_DUMP, $branches);
+              }
             }
           }
         }
+        // Set remote.
+        list($upstream['branch'], $upstream['remote']) = array_reverse(explode('/', $remote));
+        // Check for latest release tag from last common commit.
+        exec("git tag --points-at $last_base $pattern.* 2> " . GIT_DEPLOY_ERROR_DUMP, $tags);
+        if (!empty($tags)) {
+          usort($tags, 'version_compare');
+          $upstream['tag'] = end($tags);
+        }
+        // Get time of last common commit.
+        $upstream['datestamp'] = exec("git log -1 --pretty=format:%at $last_base 2> " . GIT_DEPLOY_ERROR_DUMP);
       }
-      if (!$tag_found) {
-        $last_tag = '';
-      }
-      // The git log -1 command always succeeds and if we are not on a
-      // tag this will happen to return the time of the last commit which
-      // is exactly what we wanted.
-      exec("$git log -1 --pretty=format:%at $last_tag 2>&1", $datestamp);
-      if ($datestamp && is_numeric($datestamp[0])) {
-        $info['datestamp'] = $datestamp[0];
-      }
+    }
+  }
+  return $upstream;
+}
 
-      if ($is_core) {
-        $core_info['version'] = $info['version'];
-        $core_info['datestamp'] = $info['datestamp'];
+/**
+ * Implements hook_update_status_alter().
+ */
+function git_deploy_update_status_alter(&$projects) {
+  foreach ($projects as $project => &$project_info) {
+    // Override the update status on dev branches.
+    if ($project_info['install_type'] == 'dev' && in_array($project_info['status'], [UpdateManagerInterface::NOT_SECURE, UpdateManagerInterface::NOT_CURRENT])) {
+      // Make sure Git operates in the right directory.
+      chdir(drupal_get_path(preg_replace('/-disabled$/', '', $project_info['project_type']), key($project_info['includes'])));
+      // Verify that we are in a Git repository.
+      $directory = exec('git rev-parse --show-toplevel 2> ' . GIT_DEPLOY_ERROR_DUMP);
+      if (!empty($directory)) {
+        $version = $project_info['existing_version'];
+        if (isset($project_info['releases'][$version])) {
+          $release = $project_info['releases'][$version];
+          if (isset($release['tag'])) {
+            // Fetch latest commit from origin.
+            exec('git fetch origin 2> ' . GIT_DEPLOY_ERROR_DUMP);
+            $last_commit = exec("git log -1 --pretty=format:%H origin/$release[tag] 2> " . GIT_DEPLOY_ERROR_DUMP);
+            // See if local branch contains latest commit.
+            exec("git merge-base --is-ancestor $last_commit HEAD 2> " . GIT_DEPLOY_ERROR_DUMP, $output, $return_val);
+            // We still need to compare commit time to release time because this
+            // repository may have been cloned from a copy of the upstream
+            // repository. Allow a 12 hour time difference between release and
+            // last commit, because dev releases are packaged only twice a day.
+            // Add a 100-second buffer to account for packaging time.
+            if ($return_val === 0 && $project_info['datestamp'] + 43200 + 100 > $release['date']) {
+              $project_info['status'] = UpdateManagerInterface::CURRENT;
+            }
+          }
+        }
       }
     }
   }
+  // Switch back to Drupal root folder.
+  chdir(DRUPAL_ROOT);
 }
-- 
2.17.2 (Apple Git-113)

