MediaWiki  master
GitInfo.php
Go to the documentation of this file.
1 <?php
29 use Wikimedia\AtEase\AtEase;
30 
36 class GitInfo {
37 
41  protected static $repo = null;
42 
46  protected $basedir;
47 
51  protected $repoDir;
52 
56  protected $cacheFile;
57 
61  protected $cache = [];
62 
66  private static $viewers = false;
67 
74  public function __construct( $repoDir, $usePrecomputed = true ) {
75  $this->repoDir = $repoDir;
76  $this->cacheFile = self::getCacheFilePath( $repoDir );
77  wfDebugLog( 'gitinfo',
78  "Candidate cacheFile={$this->cacheFile} for {$repoDir}"
79  );
80  if ( $usePrecomputed &&
81  $this->cacheFile !== null &&
82  is_readable( $this->cacheFile )
83  ) {
84  $this->cache = FormatJson::decode(
85  file_get_contents( $this->cacheFile ),
86  true
87  );
88  wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
89  }
90 
91  if ( !$this->cacheIsComplete() ) {
92  wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
93  $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
94  if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
95  $GITfile = file_get_contents( $this->basedir );
96  if ( strlen( $GITfile ) > 8 &&
97  substr( $GITfile, 0, 8 ) === 'gitdir: '
98  ) {
99  $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
100  if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
101  // Path from GITfile is absolute
102  $this->basedir = $path;
103  } else {
104  $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
105  }
106  }
107  }
108  }
109  }
110 
119  protected static function getCacheFilePath( $repoDir ) {
120  $config = MediaWikiServices::getInstance()->getMainConfig();
121  $gitInfoCacheDirectory = $config->get( MainConfigNames::GitInfoCacheDirectory );
122  if ( $gitInfoCacheDirectory === false ) {
123  $gitInfoCacheDirectory = $config->get( MainConfigNames::CacheDirectory ) . '/gitinfo';
124  }
125  $baseDir = $config->get( MainConfigNames::BaseDirectory );
126  if ( $gitInfoCacheDirectory ) {
127  // Convert both $IP and $repoDir to canonical paths to protect against
128  // $IP having changed between the settings files and runtime.
129  $realIP = realpath( $baseDir );
130  $repoName = realpath( $repoDir );
131  if ( $repoName === false ) {
132  // Unit tests use fake path names
133  $repoName = $repoDir;
134  }
135  if ( strpos( $repoName, $realIP ) === 0 ) {
136  // Strip $IP from path
137  $repoName = substr( $repoName, strlen( $realIP ) );
138  }
139  // Transform path to git repo to something we can safely embed in
140  // a filename
141  $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
142  $fileName = 'info' . $repoName . '.json';
143  $cachePath = "{$gitInfoCacheDirectory}/{$fileName}";
144  if ( is_readable( $cachePath ) ) {
145  return $cachePath;
146  }
147  }
148 
149  return "$repoDir/gitinfo.json";
150  }
151 
157  public static function repo() {
158  if ( self::$repo === null ) {
159  self::$repo = new self( MW_INSTALL_PATH );
160  }
161  return self::$repo;
162  }
163 
170  public static function isSHA1( $str ) {
171  return (bool)preg_match( '/^[0-9A-F]{40}$/i', $str );
172  }
173 
179  public function getHead() {
180  if ( !isset( $this->cache['head'] ) ) {
181  $headFile = "{$this->basedir}/HEAD";
182  $head = false;
183 
184  if ( is_readable( $headFile ) ) {
185  $head = file_get_contents( $headFile );
186 
187  if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
188  $head = rtrim( $m[1] );
189  } else {
190  $head = rtrim( $head );
191  }
192  }
193  $this->cache['head'] = $head;
194  }
195  return $this->cache['head'];
196  }
197 
203  public function getHeadSHA1() {
204  if ( !isset( $this->cache['headSHA1'] ) ) {
205  $head = $this->getHead();
206  $sha1 = false;
207 
208  // If detached HEAD may be a SHA1
209  if ( self::isSHA1( $head ) ) {
210  $sha1 = $head;
211  } else {
212  // If not a SHA1 it may be a ref:
213  $refFile = "{$this->basedir}/{$head}";
214  $packedRefs = "{$this->basedir}/packed-refs";
215  $headRegex = preg_quote( $head, '/' );
216  if ( is_readable( $refFile ) ) {
217  $sha1 = rtrim( file_get_contents( $refFile ) );
218  } elseif ( is_readable( $packedRefs ) &&
219  preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches )
220  ) {
221  $sha1 = $matches[1];
222  }
223  }
224  $this->cache['headSHA1'] = $sha1;
225  }
226  return $this->cache['headSHA1'];
227  }
228 
235  public function getHeadCommitDate() {
236  $gitBin = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::GitBin );
237 
238  if ( !isset( $this->cache['headCommitDate'] ) ) {
239  $date = false;
240 
241  // Suppress warnings about any open_basedir restrictions affecting $wgGitBin (T74445).
242  $isFile = AtEase::quietCall( 'is_file', $gitBin );
243  if ( $isFile &&
244  is_executable( $gitBin ) &&
245  !Shell::isDisabled() &&
246  $this->getHead() !== false
247  ) {
248  $cmd = [
249  $gitBin,
250  'show',
251  '-s',
252  '--format=format:%ct',
253  'HEAD',
254  ];
255  $gitDir = realpath( $this->basedir );
256  $result = Shell::command( $cmd )
257  ->environment( [ 'GIT_DIR' => $gitDir ] )
258  ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
259  ->whitelistPaths( [ $gitDir, $this->repoDir ] )
260  ->execute();
261 
262  if ( $result->getExitCode() === 0 ) {
263  $date = (int)$result->getStdout();
264  }
265  }
266  $this->cache['headCommitDate'] = $date;
267  }
268  return $this->cache['headCommitDate'];
269  }
270 
276  public function getCurrentBranch() {
277  if ( !isset( $this->cache['branch'] ) ) {
278  $branch = $this->getHead();
279  if ( $branch &&
280  preg_match( "#^refs/heads/(.*)$#", $branch, $m )
281  ) {
282  $branch = $m[1];
283  }
284  $this->cache['branch'] = $branch;
285  }
286  return $this->cache['branch'];
287  }
288 
294  public function getHeadViewUrl() {
295  $url = $this->getRemoteUrl();
296  if ( $url === false ) {
297  return false;
298  }
299  foreach ( self::getViewers() as $repo => $viewer ) {
300  $pattern = '#^' . $repo . '$#';
301  if ( preg_match( $pattern, $url, $matches ) ) {
302  $viewerUrl = preg_replace( $pattern, $viewer, $url );
303  $headSHA1 = $this->getHeadSHA1();
304  $replacements = [
305  '%h' => substr( $headSHA1, 0, 7 ),
306  '%H' => $headSHA1,
307  '%r' => urlencode( $matches[1] ),
308  '%R' => $matches[1],
309  ];
310  return strtr( $viewerUrl, $replacements );
311  }
312  }
313  return false;
314  }
315 
320  protected function getRemoteUrl() {
321  if ( !isset( $this->cache['remoteURL'] ) ) {
322  $config = "{$this->basedir}/config";
323  $url = false;
324  if ( is_readable( $config ) ) {
325  AtEase::suppressWarnings();
326  $configArray = parse_ini_file( $config, true );
327  AtEase::restoreWarnings();
328  $remote = false;
329 
330  // Use the "origin" remote repo if available or any other repo if not.
331  if ( isset( $configArray['remote origin'] ) ) {
332  $remote = $configArray['remote origin'];
333  } elseif ( is_array( $configArray ) ) {
334  foreach ( $configArray as $sectionName => $sectionConf ) {
335  if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
336  $remote = $sectionConf;
337  }
338  }
339  }
340 
341  if ( $remote !== false && isset( $remote['url'] ) ) {
342  $url = $remote['url'];
343  }
344  }
345  $this->cache['remoteURL'] = $url;
346  }
347  return $this->cache['remoteURL'];
348  }
349 
359  public function cacheIsComplete() {
360  return isset( $this->cache['head'] ) &&
361  isset( $this->cache['headSHA1'] ) &&
362  isset( $this->cache['headCommitDate'] ) &&
363  isset( $this->cache['branch'] ) &&
364  isset( $this->cache['remoteURL'] );
365  }
366 
376  public function precomputeValues() {
377  if ( $this->cacheFile !== null ) {
378  // Try to completely populate the cache
379  $this->getHead();
380  $this->getHeadSHA1();
381  $this->getHeadCommitDate();
382  $this->getCurrentBranch();
383  $this->getRemoteUrl();
384 
385  if ( !$this->cacheIsComplete() ) {
386  wfDebugLog( 'gitinfo',
387  "Failed to compute GitInfo for \"{$this->basedir}\""
388  );
389  return;
390  }
391 
392  $cacheDir = dirname( $this->cacheFile );
393  if ( !file_exists( $cacheDir ) &&
394  !wfMkdirParents( $cacheDir, null, __METHOD__ )
395  ) {
396  throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
397  }
398 
399  file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
400  }
401  }
402 
407  public static function headSHA1() {
408  return self::repo()->getHeadSHA1();
409  }
410 
415  public static function currentBranch() {
416  return self::repo()->getCurrentBranch();
417  }
418 
423  public static function headViewUrl() {
424  return self::repo()->getHeadViewUrl();
425  }
426 
431  protected static function getViewers() {
432  $gitRepositoryViewers = MediaWikiServices::getInstance()->getMainConfig()
433  ->get( MainConfigNames::GitRepositoryViewers );
434 
435  if ( self::$viewers === false ) {
436  self::$viewers = $gitRepositoryViewers;
437  Hooks::runner()->onGitViewers( self::$viewers );
438  }
439 
440  return self::$viewers;
441  }
442 }
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
$matches
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:146
static repo()
Get the singleton for the repo at MW_INSTALL_PATH.
Definition: GitInfo.php:157
getHead()
Get the HEAD of the repo (without any opening "ref: ")
Definition: GitInfo.php:179
static getCacheFilePath( $repoDir)
Compute the path to the cache file for a given directory.
Definition: GitInfo.php:119
static array false $viewers
Map of repo URLs to viewer URLs.
Definition: GitInfo.php:66
$cacheFile
Path to JSON cache file for pre-computed git information.
Definition: GitInfo.php:56
$basedir
Location of the .git directory.
Definition: GitInfo.php:46
getRemoteUrl()
Get the URL of the remote origin.
Definition: GitInfo.php:320
static headSHA1()
Definition: GitInfo.php:407
getHeadCommitDate()
Get the commit date of HEAD entry of the git code repository.
Definition: GitInfo.php:235
static headViewUrl()
Definition: GitInfo.php:423
precomputeValues()
Precompute and cache git information.
Definition: GitInfo.php:376
$cache
Cached git information.
Definition: GitInfo.php:61
cacheIsComplete()
Check to see if the current cache is fully populated.
Definition: GitInfo.php:359
static $repo
Singleton for the repo at $IP.
Definition: GitInfo.php:41
$repoDir
Location of the repository.
Definition: GitInfo.php:51
static currentBranch()
Definition: GitInfo.php:415
getHeadViewUrl()
Get an URL to a web viewer link to the HEAD revision.
Definition: GitInfo.php:294
__construct( $repoDir, $usePrecomputed=true)
Definition: GitInfo.php:74
static isSHA1( $str)
Check if a string looks like a hex encoded SHA1 hash.
Definition: GitInfo.php:170
getHeadSHA1()
Get the SHA1 for the current HEAD of the repo.
Definition: GitInfo.php:203
getCurrentBranch()
Get the name of the current branch, or HEAD if not found.
Definition: GitInfo.php:276
static getViewers()
Gets the list of repository viewers.
Definition: GitInfo.php:431
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
MediaWiki exception.
Definition: MWException.php:29
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Executes shell commands.
Definition: Shell.php:46