MediaWiki  master
GitInfo.php
Go to the documentation of this file.
1 <?php
27 use Wikimedia\AtEase\AtEase;
28 
34 class GitInfo {
35 
39  protected static $repo = null;
40 
44  protected $basedir;
45 
49  protected $repoDir;
50 
54  protected $cacheFile;
55 
59  protected $cache = [];
60 
64  private static $viewers = false;
65 
72  public function __construct( $repoDir, $usePrecomputed = true ) {
73  $this->repoDir = $repoDir;
74  $this->cacheFile = self::getCacheFilePath( $repoDir );
75  wfDebugLog( 'gitinfo',
76  "Candidate cacheFile={$this->cacheFile} for {$repoDir}"
77  );
78  if ( $usePrecomputed &&
79  $this->cacheFile !== null &&
80  is_readable( $this->cacheFile )
81  ) {
82  $this->cache = FormatJson::decode(
83  file_get_contents( $this->cacheFile ),
84  true
85  );
86  wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
87  }
88 
89  if ( !$this->cacheIsComplete() ) {
90  wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
91  $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
92  if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
93  $GITfile = file_get_contents( $this->basedir );
94  if ( strlen( $GITfile ) > 8 &&
95  substr( $GITfile, 0, 8 ) === 'gitdir: '
96  ) {
97  $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
98  if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
99  // Path from GITfile is absolute
100  $this->basedir = $path;
101  } else {
102  $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
103  }
104  }
105  }
106  }
107  }
108 
117  protected static function getCacheFilePath( $repoDir ) {
119 
120  if ( $wgGitInfoCacheDirectory ) {
121  // Convert both $IP and $repoDir to canonical paths to protect against
122  // $IP having changed between the settings files and runtime.
123  $realIP = realpath( $IP );
124  $repoName = realpath( $repoDir );
125  if ( $repoName === false ) {
126  // Unit tests use fake path names
127  $repoName = $repoDir;
128  }
129  if ( strpos( $repoName, $realIP ) === 0 ) {
130  // Strip $IP from path
131  $repoName = substr( $repoName, strlen( $realIP ) );
132  }
133  // Transform path to git repo to something we can safely embed in
134  // a filename
135  $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
136  $fileName = 'info' . $repoName . '.json';
137  $cachePath = "{$wgGitInfoCacheDirectory}/{$fileName}";
138  if ( is_readable( $cachePath ) ) {
139  return $cachePath;
140  }
141  }
142 
143  return "$repoDir/gitinfo.json";
144  }
145 
151  public static function repo() {
152  if ( self::$repo === null ) {
153  global $IP;
154  self::$repo = new self( $IP );
155  }
156  return self::$repo;
157  }
158 
165  public static function isSHA1( $str ) {
166  return (bool)preg_match( '/^[0-9A-F]{40}$/i', $str );
167  }
168 
174  public function getHead() {
175  if ( !isset( $this->cache['head'] ) ) {
176  $headFile = "{$this->basedir}/HEAD";
177  $head = false;
178 
179  if ( is_readable( $headFile ) ) {
180  $head = file_get_contents( $headFile );
181 
182  if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
183  $head = rtrim( $m[1] );
184  } else {
185  $head = rtrim( $head );
186  }
187  }
188  $this->cache['head'] = $head;
189  }
190  return $this->cache['head'];
191  }
192 
198  public function getHeadSHA1() {
199  if ( !isset( $this->cache['headSHA1'] ) ) {
200  $head = $this->getHead();
201  $sha1 = false;
202 
203  // If detached HEAD may be a SHA1
204  if ( self::isSHA1( $head ) ) {
205  $sha1 = $head;
206  } else {
207  // If not a SHA1 it may be a ref:
208  $refFile = "{$this->basedir}/{$head}";
209  $packedRefs = "{$this->basedir}/packed-refs";
210  $headRegex = preg_quote( $head, '/' );
211  if ( is_readable( $refFile ) ) {
212  $sha1 = rtrim( file_get_contents( $refFile ) );
213  } elseif ( is_readable( $packedRefs ) &&
214  preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches )
215  ) {
216  $sha1 = $matches[1];
217  }
218  }
219  $this->cache['headSHA1'] = $sha1;
220  }
221  return $this->cache['headSHA1'];
222  }
223 
230  public function getHeadCommitDate() {
231  global $wgGitBin;
232 
233  if ( !isset( $this->cache['headCommitDate'] ) ) {
234  $date = false;
235 
236  // Suppress warnings about any open_basedir restrictions affecting $wgGitBin (T74445).
237  $isFile = AtEase::quietCall( 'is_file', $wgGitBin );
238  if ( $isFile &&
239  is_executable( $wgGitBin ) &&
240  !Shell::isDisabled() &&
241  $this->getHead() !== false
242  ) {
243  $cmd = [
244  $wgGitBin,
245  'show',
246  '-s',
247  '--format=format:%ct',
248  'HEAD',
249  ];
250  $gitDir = realpath( $this->basedir );
251  $result = Shell::command( $cmd )
252  ->environment( [ 'GIT_DIR' => $gitDir ] )
253  ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
254  ->whitelistPaths( [ $gitDir, $this->repoDir ] )
255  ->execute();
256 
257  if ( $result->getExitCode() === 0 ) {
258  $date = (int)$result->getStdout();
259  }
260  }
261  $this->cache['headCommitDate'] = $date;
262  }
263  return $this->cache['headCommitDate'];
264  }
265 
271  public function getCurrentBranch() {
272  if ( !isset( $this->cache['branch'] ) ) {
273  $branch = $this->getHead();
274  if ( $branch &&
275  preg_match( "#^refs/heads/(.*)$#", $branch, $m )
276  ) {
277  $branch = $m[1];
278  }
279  $this->cache['branch'] = $branch;
280  }
281  return $this->cache['branch'];
282  }
283 
289  public function getHeadViewUrl() {
290  $url = $this->getRemoteUrl();
291  if ( $url === false ) {
292  return false;
293  }
294  foreach ( self::getViewers() as $repo => $viewer ) {
295  $pattern = '#^' . $repo . '$#';
296  if ( preg_match( $pattern, $url, $matches ) ) {
297  $viewerUrl = preg_replace( $pattern, $viewer, $url );
298  $headSHA1 = $this->getHeadSHA1();
299  $replacements = [
300  '%h' => substr( $headSHA1, 0, 7 ),
301  '%H' => $headSHA1,
302  '%r' => urlencode( $matches[1] ),
303  '%R' => $matches[1],
304  ];
305  return strtr( $viewerUrl, $replacements );
306  }
307  }
308  return false;
309  }
310 
315  protected function getRemoteUrl() {
316  if ( !isset( $this->cache['remoteURL'] ) ) {
317  $config = "{$this->basedir}/config";
318  $url = false;
319  if ( is_readable( $config ) ) {
320  Wikimedia\suppressWarnings();
321  $configArray = parse_ini_file( $config, true );
322  Wikimedia\restoreWarnings();
323  $remote = false;
324 
325  // Use the "origin" remote repo if available or any other repo if not.
326  if ( isset( $configArray['remote origin'] ) ) {
327  $remote = $configArray['remote origin'];
328  } elseif ( is_array( $configArray ) ) {
329  foreach ( $configArray as $sectionName => $sectionConf ) {
330  if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
331  $remote = $sectionConf;
332  }
333  }
334  }
335 
336  if ( $remote !== false && isset( $remote['url'] ) ) {
337  $url = $remote['url'];
338  }
339  }
340  $this->cache['remoteURL'] = $url;
341  }
342  return $this->cache['remoteURL'];
343  }
344 
354  public function cacheIsComplete() {
355  return isset( $this->cache['head'] ) &&
356  isset( $this->cache['headSHA1'] ) &&
357  isset( $this->cache['headCommitDate'] ) &&
358  isset( $this->cache['branch'] ) &&
359  isset( $this->cache['remoteURL'] );
360  }
361 
371  public function precomputeValues() {
372  if ( $this->cacheFile !== null ) {
373  // Try to completely populate the cache
374  $this->getHead();
375  $this->getHeadSHA1();
376  $this->getHeadCommitDate();
377  $this->getCurrentBranch();
378  $this->getRemoteUrl();
379 
380  if ( !$this->cacheIsComplete() ) {
381  wfDebugLog( 'gitinfo',
382  "Failed to compute GitInfo for \"{$this->basedir}\""
383  );
384  return;
385  }
386 
387  $cacheDir = dirname( $this->cacheFile );
388  if ( !file_exists( $cacheDir ) &&
389  !wfMkdirParents( $cacheDir, null, __METHOD__ )
390  ) {
391  throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
392  }
393 
394  file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
395  }
396  }
397 
402  public static function headSHA1() {
403  return self::repo()->getHeadSHA1();
404  }
405 
410  public static function currentBranch() {
411  return self::repo()->getCurrentBranch();
412  }
413 
418  public static function headViewUrl() {
419  return self::repo()->getHeadViewUrl();
420  }
421 
426  protected static function getViewers() {
428 
429  if ( self::$viewers === false ) {
430  self::$viewers = $wgGitRepositoryViewers;
431  Hooks::runner()->onGitViewers( self::$viewers );
432  }
433 
434  return self::$viewers;
435  }
436 }
GitInfo\getRemoteUrl
getRemoteUrl()
Get the URL of the remote origin.
Definition: GitInfo.php:315
GitInfo\getHead
getHead()
Get the HEAD of the repo (without any opening "ref: ")
Definition: GitInfo.php:174
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:44
GitInfo\getHeadCommitDate
getHeadCommitDate()
Get the commit date of HEAD entry of the git code repository.
Definition: GitInfo.php:230
GitInfo\$repoDir
$repoDir
Location of the repository.
Definition: GitInfo.php:49
wfMkdirParents
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
Definition: GlobalFunctions.php:1900
GitInfo\isSHA1
static isSHA1( $str)
Check if a string looks like a hex encoded SHA1 hash.
Definition: GitInfo.php:165
GitInfo\$cacheFile
$cacheFile
Path to JSON cache file for pre-computed git information.
Definition: GitInfo.php:54
GitInfo\headSHA1
static headSHA1()
Definition: GitInfo.php:402
GitInfo\headViewUrl
static headViewUrl()
Definition: GitInfo.php:418
GitInfo\$cache
$cache
Cached git information.
Definition: GitInfo.php:59
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:992
GitInfo\precomputeValues
precomputeValues()
Precompute and cache git information.
Definition: GitInfo.php:371
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
GitInfo\getHeadSHA1
getHeadSHA1()
Get the SHA1 for the current HEAD of the repo.
Definition: GitInfo.php:198
GitInfo\repo
static repo()
Get the singleton for the repo at $IP.
Definition: GitInfo.php:151
MWException
MediaWiki exception.
Definition: MWException.php:29
$matches
$matches
Definition: NoLocalSettings.php:24
GitInfo\__construct
__construct( $repoDir, $usePrecomputed=true)
Stable to call.
Definition: GitInfo.php:72
GitInfo\cacheIsComplete
cacheIsComplete()
Check to see if the current cache is fully populated.
Definition: GitInfo.php:354
GitInfo\$viewers
static array false $viewers
Map of repo URLs to viewer URLs.
Definition: GitInfo.php:64
GitInfo
@newable
Definition: GitInfo.php:34
$wgGitBin
$wgGitBin
Fully specified path to git binary.
Definition: DefaultSettings.php:7188
GitInfo\getViewers
static getViewers()
Gets the list of repository viewers.
Definition: GitInfo.php:426
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
GitInfo\currentBranch
static currentBranch()
Definition: GitInfo.php:410
GitInfo\getHeadViewUrl
getHeadViewUrl()
Get an URL to a web viewer link to the HEAD revision.
Definition: GitInfo.php:289
GitInfo\$basedir
$basedir
Location of the .git directory.
Definition: GitInfo.php:44
$wgGitInfoCacheDirectory
$wgGitInfoCacheDirectory
Directory where GitInfo will look for pre-computed cache files.
Definition: DefaultSettings.php:2797
$wgGitRepositoryViewers
$wgGitRepositoryViewers
Map GIT repository URLs to viewer URLs to provide links in Special:Version.
Definition: DefaultSettings.php:7203
GitInfo\$repo
static $repo
Singleton for the repo at $IP.
Definition: GitInfo.php:39
$path
$path
Definition: NoLocalSettings.php:25
GitInfo\getCurrentBranch
getCurrentBranch()
Get the name of the current branch, or HEAD if not found.
Definition: GitInfo.php:271
$IP
$IP
Definition: WebStart.php:49
GitInfo\getCacheFilePath
static getCacheFilePath( $repoDir)
Compute the path to the cache file for a given directory.
Definition: GitInfo.php:117