[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/ -> GitInfo.php (source)

   1  <?php
   2  /**
   3   * A class to help return information about a git repo MediaWiki may be inside
   4   * This is used by Special:Version and is also useful for the LocalSettings.php
   5   * of anyone working on large branches in git to setup config that show up only
   6   * when specific branches are currently checked out.
   7   *
   8   * This program is free software; you can redistribute it and/or modify
   9   * it under the terms of the GNU General Public License as published by
  10   * the Free Software Foundation; either version 2 of the License, or
  11   * (at your option) any later version.
  12   *
  13   * This program is distributed in the hope that it will be useful,
  14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16   * GNU General Public License for more details.
  17   *
  18   * You should have received a copy of the GNU General Public License along
  19   * with this program; if not, write to the Free Software Foundation, Inc.,
  20   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21   * http://www.gnu.org/copyleft/gpl.html
  22   *
  23   * @file
  24   */
  25  
  26  class GitInfo {
  27  
  28      /**
  29       * Singleton for the repo at $IP
  30       */
  31      protected static $repo = null;
  32  
  33      /**
  34       * Location of the .git directory
  35       */
  36      protected $basedir;
  37  
  38      /**
  39       * Path to JSON cache file for pre-computed git information.
  40       */
  41      protected $cacheFile;
  42  
  43      /**
  44       * Cached git information.
  45       */
  46      protected $cache = array();
  47  
  48      /**
  49       * Map of repo URLs to viewer URLs. Access via static method getViewers().
  50       */
  51      private static $viewers = false;
  52  
  53      /**
  54       * @param string $repoDir The root directory of the repo where .git can be found
  55       * @param bool $usePrecomputed Use precomputed information if available
  56       * @see precomputeValues
  57       */
  58  	public function __construct( $repoDir, $usePrecomputed = true ) {
  59          $this->cacheFile = self::getCacheFilePath( $repoDir );
  60          wfDebugLog( 'gitinfo',
  61              "Computed cacheFile={$this->cacheFile} for {$repoDir}"
  62          );
  63          if ( $usePrecomputed &&
  64              $this->cacheFile !== null &&
  65              is_readable( $this->cacheFile )
  66          ) {
  67              $this->cache = FormatJson::decode(
  68                  file_get_contents( $this->cacheFile ),
  69                  true
  70              );
  71              wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
  72          }
  73  
  74          if ( !$this->cacheIsComplete() ) {
  75              wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
  76              $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
  77              if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
  78                  $GITfile = file_get_contents( $this->basedir );
  79                  if ( strlen( $GITfile ) > 8 &&
  80                      substr( $GITfile, 0, 8 ) === 'gitdir: '
  81                  ) {
  82                      $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
  83                      if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
  84                          // Path from GITfile is absolute
  85                          $this->basedir = $path;
  86                      } else {
  87                          $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
  88                      }
  89                  }
  90              }
  91          }
  92      }
  93  
  94      /**
  95       * Compute the path to the cache file for a given directory.
  96       *
  97       * @param string $repoDir The root directory of the repo where .git can be found
  98       * @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or
  99       * null if $wgGitInfoCacheDirectory is false (cache disabled).
 100       * @since 1.24
 101       */
 102  	protected static function getCacheFilePath( $repoDir ) {
 103          global $IP, $wgGitInfoCacheDirectory;
 104  
 105          if ( $wgGitInfoCacheDirectory ) {
 106              // Convert both $IP and $repoDir to canonical paths to protect against
 107              // $IP having changed between the settings files and runtime.
 108              $realIP = realpath( $IP );
 109              $repoName = realpath( $repoDir );
 110              if ( $repoName === false ) {
 111                  // Unit tests use fake path names
 112                  $repoName = $repoDir;
 113              }
 114              if ( strpos( $repoName, $realIP ) === 0 ) {
 115                  // Strip $IP from path
 116                  $repoName = substr( $repoName, strlen( $realIP ) );
 117              }
 118              // Transform path to git repo to something we can safely embed in
 119              // a filename
 120              $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
 121              $fileName = 'info' . $repoName . '.json';
 122              return "{$wgGitInfoCacheDirectory}/{$fileName}";
 123          }
 124          return null;
 125      }
 126  
 127      /**
 128       * Get the singleton for the repo at $IP
 129       *
 130       * @return GitInfo
 131       */
 132  	public static function repo() {
 133          if ( is_null( self::$repo ) ) {
 134              global $IP;
 135              self::$repo = new self( $IP );
 136          }
 137          return self::$repo;
 138      }
 139  
 140      /**
 141       * Check if a string looks like a hex encoded SHA1 hash
 142       *
 143       * @param string $str The string to check
 144       * @return bool Whether or not the string looks like a SHA1
 145       */
 146  	public static function isSHA1( $str ) {
 147          return !!preg_match( '/^[0-9A-F]{40}$/i', $str );
 148      }
 149  
 150      /**
 151       * Get the HEAD of the repo (without any opening "ref: ")
 152       *
 153       * @return string|bool The HEAD (git reference or SHA1) or false
 154       */
 155  	public function getHead() {
 156          if ( !isset( $this->cache['head'] ) ) {
 157              $headFile = "{$this->basedir}/HEAD";
 158              $head = false;
 159  
 160              if ( is_readable( $headFile ) ) {
 161                  $head = file_get_contents( $headFile );
 162  
 163                  if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
 164                      $head = rtrim( $m[1] );
 165                  } else {
 166                      $head = rtrim( $head );
 167                  }
 168              }
 169              $this->cache['head'] = $head;
 170          }
 171          return $this->cache['head'];
 172      }
 173  
 174      /**
 175       * Get the SHA1 for the current HEAD of the repo
 176       *
 177       * @return string|bool A SHA1 or false
 178       */
 179  	public function getHeadSHA1() {
 180          if ( !isset( $this->cache['headSHA1'] ) ) {
 181              $head = $this->getHead();
 182              $sha1 = false;
 183  
 184              // If detached HEAD may be a SHA1
 185              if ( self::isSHA1( $head ) ) {
 186                  $sha1 = $head;
 187              } else {
 188                  // If not a SHA1 it may be a ref:
 189                  $refFile = "{$this->basedir}/{$head}";
 190                  if ( is_readable( $refFile ) ) {
 191                      $sha1 = rtrim( file_get_contents( $refFile ) );
 192                  }
 193              }
 194              $this->cache['headSHA1'] = $sha1;
 195          }
 196          return $this->cache['headSHA1'];
 197      }
 198  
 199      /**
 200       * Get the commit date of HEAD entry of the git code repository
 201       *
 202       * @since 1.22
 203       * @return int|bool Commit date (UNIX timestamp) or false
 204       */
 205  	public function getHeadCommitDate() {
 206          global $wgGitBin;
 207  
 208          if ( !isset( $this->cache['headCommitDate'] ) ) {
 209              $date = false;
 210              if ( is_file( $wgGitBin ) &&
 211                  is_executable( $wgGitBin ) &&
 212                  $this->getHead() !== false
 213              ) {
 214                  $environment = array( "GIT_DIR" => $this->basedir );
 215                  $cmd = wfEscapeShellArg( $wgGitBin ) .
 216                      " show -s --format=format:%ct HEAD";
 217                  $retc = false;
 218                  $commitDate = wfShellExec( $cmd, $retc, $environment );
 219                  if ( $retc === 0 ) {
 220                      $date = (int)$commitDate;
 221                  }
 222              }
 223              $this->cache['headCommitDate'] = $date;
 224          }
 225          return $this->cache['headCommitDate'];
 226      }
 227  
 228      /**
 229       * Get the name of the current branch, or HEAD if not found
 230       *
 231       * @return string|bool The branch name, HEAD, or false
 232       */
 233  	public function getCurrentBranch() {
 234          if ( !isset( $this->cache['branch'] ) ) {
 235              $branch = $this->getHead();
 236              if ( $branch &&
 237                  preg_match( "#^refs/heads/(.*)$#", $branch, $m )
 238              ) {
 239                  $branch = $m[1];
 240              }
 241              $this->cache['branch'] = $branch;
 242          }
 243          return $this->cache['branch'];
 244      }
 245  
 246      /**
 247       * Get an URL to a web viewer link to the HEAD revision.
 248       *
 249       * @return string|bool String if a URL is available or false otherwise
 250       */
 251  	public function getHeadViewUrl() {
 252          $url = $this->getRemoteUrl();
 253          if ( $url === false ) {
 254              return false;
 255          }
 256          if ( substr( $url, -4 ) !== '.git' ) {
 257              $url .= '.git';
 258          }
 259          foreach ( self::getViewers() as $repo => $viewer ) {
 260              $pattern = '#^' . $repo . '$#';
 261              if ( preg_match( $pattern, $url, $matches ) ) {
 262                  $viewerUrl = preg_replace( $pattern, $viewer, $url );
 263                  $headSHA1 = $this->getHeadSHA1();
 264                  $replacements = array(
 265                      '%h' => substr( $headSHA1, 0, 7 ),
 266                      '%H' => $headSHA1,
 267                      '%r' => urlencode( $matches[1] ),
 268                  );
 269                  return strtr( $viewerUrl, $replacements );
 270              }
 271          }
 272          return false;
 273      }
 274  
 275      /**
 276       * Get the URL of the remote origin.
 277       * @return string|bool String if a URL is available or false otherwise.
 278       */
 279  	protected function getRemoteUrl() {
 280          if ( !isset( $this->cache['remoteURL'] ) ) {
 281              $config = "{$this->basedir}/config";
 282              $url = false;
 283              if ( is_readable( $config ) ) {
 284                  wfSuppressWarnings();
 285                  $configArray = parse_ini_file( $config, true );
 286                  wfRestoreWarnings();
 287                  $remote = false;
 288  
 289                  // Use the "origin" remote repo if available or any other repo if not.
 290                  if ( isset( $configArray['remote origin'] ) ) {
 291                      $remote = $configArray['remote origin'];
 292                  } elseif ( is_array( $configArray ) ) {
 293                      foreach ( $configArray as $sectionName => $sectionConf ) {
 294                          if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
 295                              $remote = $sectionConf;
 296                          }
 297                      }
 298                  }
 299  
 300                  if ( $remote !== false && isset( $remote['url'] ) ) {
 301                      $url = $remote['url'];
 302                  }
 303              }
 304              $this->cache['remoteURL'] = $url;
 305          }
 306          return $this->cache['remoteURL'];
 307      }
 308  
 309      /**
 310       * Check to see if the current cache is fully populated.
 311       *
 312       * Note: This method is public only to make unit testing easier. There's
 313       * really no strong reason that anything other than a test should want to
 314       * call this method.
 315       *
 316       * @return bool True if all expected cache keys exist, false otherwise
 317       */
 318  	public function cacheIsComplete() {
 319          return isset( $this->cache['head'] ) &&
 320              isset( $this->cache['headSHA1'] ) &&
 321              isset( $this->cache['headCommitDate'] ) &&
 322              isset( $this->cache['branch'] ) &&
 323              isset( $this->cache['remoteURL'] );
 324      }
 325  
 326      /**
 327       * Precompute and cache git information.
 328       *
 329       * Creates a JSON file in the cache directory associated with this
 330       * GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing
 331       * the same directory to avoid needing to examine the .git directory again.
 332       *
 333       * @since 1.24
 334       */
 335  	public function precomputeValues() {
 336          if ( $this->cacheFile !== null ) {
 337              // Try to completely populate the cache
 338              $this->getHead();
 339              $this->getHeadSHA1();
 340              $this->getHeadCommitDate();
 341              $this->getCurrentBranch();
 342              $this->getRemoteUrl();
 343  
 344              if ( !$this->cacheIsComplete() ) {
 345                  wfDebugLog( 'gitinfo',
 346                      "Failed to compute GitInfo for \"{$this->basedir}\""
 347                  );
 348                  return;
 349              }
 350  
 351              $cacheDir = dirname( $this->cacheFile );
 352              if ( !file_exists( $cacheDir ) &&
 353                  !wfMkdirParents( $cacheDir, null, __METHOD__ )
 354              ) {
 355                  throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
 356              }
 357  
 358              file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
 359          }
 360      }
 361  
 362      /**
 363       * @see self::getHeadSHA1
 364       * @return string
 365       */
 366  	public static function headSHA1() {
 367          return self::repo()->getHeadSHA1();
 368      }
 369  
 370      /**
 371       * @see self::getCurrentBranch
 372       * @return string
 373       */
 374  	public static function currentBranch() {
 375          return self::repo()->getCurrentBranch();
 376      }
 377  
 378      /**
 379       * @see self::getHeadViewUrl()
 380       * @return bool|string
 381       */
 382  	public static function headViewUrl() {
 383          return self::repo()->getHeadViewUrl();
 384      }
 385  
 386      /**
 387       * Gets the list of repository viewers
 388       * @return array
 389       */
 390  	protected static function getViewers() {
 391          global $wgGitRepositoryViewers;
 392  
 393          if ( self::$viewers === false ) {
 394              self::$viewers = $wgGitRepositoryViewers;
 395              wfRunHooks( 'GitViewers', array( &self::$viewers ) );
 396          }
 397  
 398          return self::$viewers;
 399      }
 400  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1