MediaWiki  master
GitInfo.php
Go to the documentation of this file.
1 <?php
27 
28 class GitInfo {
29 
33  protected static $repo = null;
34 
38  protected $basedir;
39 
43  protected $repoDir;
44 
48  protected $cacheFile;
49 
53  protected $cache = [];
54 
58  private static $viewers = false;
59 
65  public function __construct( $repoDir, $usePrecomputed = true ) {
66  $this->repoDir = $repoDir;
67  $this->cacheFile = self::getCacheFilePath( $repoDir );
68  wfDebugLog( 'gitinfo',
69  "Candidate cacheFile={$this->cacheFile} for {$repoDir}"
70  );
71  if ( $usePrecomputed &&
72  $this->cacheFile !== null &&
73  is_readable( $this->cacheFile )
74  ) {
75  $this->cache = FormatJson::decode(
76  file_get_contents( $this->cacheFile ),
77  true
78  );
79  wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
80  }
81 
82  if ( !$this->cacheIsComplete() ) {
83  wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
84  $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
85  if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
86  $GITfile = file_get_contents( $this->basedir );
87  if ( strlen( $GITfile ) > 8 &&
88  substr( $GITfile, 0, 8 ) === 'gitdir: '
89  ) {
90  $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
91  if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
92  // Path from GITfile is absolute
93  $this->basedir = $path;
94  } else {
95  $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
96  }
97  }
98  }
99  }
100  }
101 
110  protected static function getCacheFilePath( $repoDir ) {
112 
113  if ( $wgGitInfoCacheDirectory ) {
114  // Convert both $IP and $repoDir to canonical paths to protect against
115  // $IP having changed between the settings files and runtime.
116  $realIP = realpath( $IP );
117  $repoName = realpath( $repoDir );
118  if ( $repoName === false ) {
119  // Unit tests use fake path names
120  $repoName = $repoDir;
121  }
122  if ( strpos( $repoName, $realIP ) === 0 ) {
123  // Strip $IP from path
124  $repoName = substr( $repoName, strlen( $realIP ) );
125  }
126  // Transform path to git repo to something we can safely embed in
127  // a filename
128  $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
129  $fileName = 'info' . $repoName . '.json';
130  $cachePath = "{$wgGitInfoCacheDirectory}/{$fileName}";
131  if ( is_readable( $cachePath ) ) {
132  return $cachePath;
133  }
134  }
135 
136  return "$repoDir/gitinfo.json";
137  }
138 
144  public static function repo() {
145  if ( is_null( self::$repo ) ) {
146  global $IP;
147  self::$repo = new self( $IP );
148  }
149  return self::$repo;
150  }
151 
158  public static function isSHA1( $str ) {
159  return (bool)preg_match( '/^[0-9A-F]{40}$/i', $str );
160  }
161 
167  public function getHead() {
168  if ( !isset( $this->cache['head'] ) ) {
169  $headFile = "{$this->basedir}/HEAD";
170  $head = false;
171 
172  if ( is_readable( $headFile ) ) {
173  $head = file_get_contents( $headFile );
174 
175  if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
176  $head = rtrim( $m[1] );
177  } else {
178  $head = rtrim( $head );
179  }
180  }
181  $this->cache['head'] = $head;
182  }
183  return $this->cache['head'];
184  }
185 
191  public function getHeadSHA1() {
192  if ( !isset( $this->cache['headSHA1'] ) ) {
193  $head = $this->getHead();
194  $sha1 = false;
195 
196  // If detached HEAD may be a SHA1
197  if ( self::isSHA1( $head ) ) {
198  $sha1 = $head;
199  } else {
200  // If not a SHA1 it may be a ref:
201  $refFile = "{$this->basedir}/{$head}";
202  $packedRefs = "{$this->basedir}/packed-refs";
203  $headRegex = preg_quote( $head, '/' );
204  if ( is_readable( $refFile ) ) {
205  $sha1 = rtrim( file_get_contents( $refFile ) );
206  } elseif ( is_readable( $packedRefs ) &&
207  preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches )
208  ) {
209  $sha1 = $matches[1];
210  }
211  }
212  $this->cache['headSHA1'] = $sha1;
213  }
214  return $this->cache['headSHA1'];
215  }
216 
223  public function getHeadCommitDate() {
224  global $wgGitBin;
225 
226  if ( !isset( $this->cache['headCommitDate'] ) ) {
227  $date = false;
228  if ( is_file( $wgGitBin ) &&
229  is_executable( $wgGitBin ) &&
230  !Shell::isDisabled() &&
231  $this->getHead() !== false
232  ) {
233  $cmd = [
234  $wgGitBin,
235  'show',
236  '-s',
237  '--format=format:%ct',
238  'HEAD',
239  ];
240  $gitDir = realpath( $this->basedir );
241  $result = Shell::command( $cmd )
242  ->environment( [ 'GIT_DIR' => $gitDir ] )
243  ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
244  ->whitelistPaths( [ $gitDir, $this->repoDir ] )
245  ->execute();
246 
247  if ( $result->getExitCode() === 0 ) {
248  $date = (int)$result->getStdout();
249  }
250  }
251  $this->cache['headCommitDate'] = $date;
252  }
253  return $this->cache['headCommitDate'];
254  }
255 
261  public function getCurrentBranch() {
262  if ( !isset( $this->cache['branch'] ) ) {
263  $branch = $this->getHead();
264  if ( $branch &&
265  preg_match( "#^refs/heads/(.*)$#", $branch, $m )
266  ) {
267  $branch = $m[1];
268  }
269  $this->cache['branch'] = $branch;
270  }
271  return $this->cache['branch'];
272  }
273 
279  public function getHeadViewUrl() {
280  $url = $this->getRemoteUrl();
281  if ( $url === false ) {
282  return false;
283  }
284  foreach ( self::getViewers() as $repo => $viewer ) {
285  $pattern = '#^' . $repo . '$#';
286  if ( preg_match( $pattern, $url, $matches ) ) {
287  $viewerUrl = preg_replace( $pattern, $viewer, $url );
288  $headSHA1 = $this->getHeadSHA1();
289  $replacements = [
290  '%h' => substr( $headSHA1, 0, 7 ),
291  '%H' => $headSHA1,
292  '%r' => urlencode( $matches[1] ),
293  '%R' => $matches[1],
294  ];
295  return strtr( $viewerUrl, $replacements );
296  }
297  }
298  return false;
299  }
300 
305  protected function getRemoteUrl() {
306  if ( !isset( $this->cache['remoteURL'] ) ) {
307  $config = "{$this->basedir}/config";
308  $url = false;
309  if ( is_readable( $config ) ) {
310  Wikimedia\suppressWarnings();
311  $configArray = parse_ini_file( $config, true );
312  Wikimedia\restoreWarnings();
313  $remote = false;
314 
315  // Use the "origin" remote repo if available or any other repo if not.
316  if ( isset( $configArray['remote origin'] ) ) {
317  $remote = $configArray['remote origin'];
318  } elseif ( is_array( $configArray ) ) {
319  foreach ( $configArray as $sectionName => $sectionConf ) {
320  if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
321  $remote = $sectionConf;
322  }
323  }
324  }
325 
326  if ( $remote !== false && isset( $remote['url'] ) ) {
327  $url = $remote['url'];
328  }
329  }
330  $this->cache['remoteURL'] = $url;
331  }
332  return $this->cache['remoteURL'];
333  }
334 
344  public function cacheIsComplete() {
345  return isset( $this->cache['head'] ) &&
346  isset( $this->cache['headSHA1'] ) &&
347  isset( $this->cache['headCommitDate'] ) &&
348  isset( $this->cache['branch'] ) &&
349  isset( $this->cache['remoteURL'] );
350  }
351 
361  public function precomputeValues() {
362  if ( $this->cacheFile !== null ) {
363  // Try to completely populate the cache
364  $this->getHead();
365  $this->getHeadSHA1();
366  $this->getHeadCommitDate();
367  $this->getCurrentBranch();
368  $this->getRemoteUrl();
369 
370  if ( !$this->cacheIsComplete() ) {
371  wfDebugLog( 'gitinfo',
372  "Failed to compute GitInfo for \"{$this->basedir}\""
373  );
374  return;
375  }
376 
377  $cacheDir = dirname( $this->cacheFile );
378  if ( !file_exists( $cacheDir ) &&
379  !wfMkdirParents( $cacheDir, null, __METHOD__ )
380  ) {
381  throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
382  }
383 
384  file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
385  }
386  }
387 
392  public static function headSHA1() {
393  return self::repo()->getHeadSHA1();
394  }
395 
400  public static function currentBranch() {
401  return self::repo()->getCurrentBranch();
402  }
403 
408  public static function headViewUrl() {
409  return self::repo()->getHeadViewUrl();
410  }
411 
416  protected static function getViewers() {
418 
419  if ( self::$viewers === false ) {
420  self::$viewers = $wgGitRepositoryViewers;
421  Hooks::run( 'GitViewers', [ &self::$viewers ] );
422  }
423 
424  return self::$viewers;
425  }
426 }
GitInfo\getRemoteUrl
getRemoteUrl()
Get the URL of the remote origin.
Definition: GitInfo.php:305
GitInfo\getHead
getHead()
Get the HEAD of the repo (without any opening "ref: ")
Definition: GitInfo.php:167
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:223
GitInfo\$repoDir
$repoDir
Location of the repository.
Definition: GitInfo.php:43
wfMkdirParents
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
Definition: GlobalFunctions.php:1968
GitInfo\isSHA1
static isSHA1( $str)
Check if a string looks like a hex encoded SHA1 hash.
Definition: GitInfo.php:158
GitInfo\$cacheFile
$cacheFile
Path to JSON cache file for pre-computed git information.
Definition: GitInfo.php:48
GitInfo\headSHA1
static headSHA1()
Definition: GitInfo.php:392
GitInfo\headViewUrl
static headViewUrl()
Definition: GitInfo.php:408
GitInfo\$cache
$cache
Cached git information.
Definition: GitInfo.php:53
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:1007
GitInfo\precomputeValues
precomputeValues()
Precompute and cache git information.
Definition: GitInfo.php:361
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:191
GitInfo\repo
static repo()
Get the singleton for the repo at $IP.
Definition: GitInfo.php:144
MWException
MediaWiki exception.
Definition: MWException.php:26
$matches
$matches
Definition: NoLocalSettings.php:24
GitInfo\__construct
__construct( $repoDir, $usePrecomputed=true)
Definition: GitInfo.php:65
GitInfo\cacheIsComplete
cacheIsComplete()
Check to see if the current cache is fully populated.
Definition: GitInfo.php:344
GitInfo\$viewers
static array false $viewers
Map of repo URLs to viewer URLs.
Definition: GitInfo.php:58
GitInfo
Definition: GitInfo.php:28
$wgGitBin
$wgGitBin
Fully specified path to git binary.
Definition: DefaultSettings.php:6754
GitInfo\getViewers
static getViewers()
Gets the list of repository viewers.
Definition: GitInfo.php:416
GitInfo\currentBranch
static currentBranch()
Definition: GitInfo.php:400
GitInfo\getHeadViewUrl
getHeadViewUrl()
Get an URL to a web viewer link to the HEAD revision.
Definition: GitInfo.php:279
GitInfo\$basedir
$basedir
Location of the .git directory.
Definition: GitInfo.php:38
$wgGitInfoCacheDirectory
$wgGitInfoCacheDirectory
Directory where GitInfo will look for pre-computed cache files.
Definition: DefaultSettings.php:2645
$wgGitRepositoryViewers
$wgGitRepositoryViewers
Map GIT repository URLs to viewer URLs to provide links in Special:Version.
Definition: DefaultSettings.php:6769
GitInfo\$repo
static $repo
Singleton for the repo at $IP.
Definition: GitInfo.php:33
$path
$path
Definition: NoLocalSettings.php:25
GitInfo\getCurrentBranch
getCurrentBranch()
Get the name of the current branch, or HEAD if not found.
Definition: GitInfo.php:261
$IP
$IP
Definition: WebStart.php:41
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
GitInfo\getCacheFilePath
static getCacheFilePath( $repoDir)
Compute the path to the cache file for a given directory.
Definition: GitInfo.php:110