MediaWiki REL1_37
GitInfo.php
Go to the documentation of this file.
1<?php
27use Wikimedia\AtEase\AtEase;
28
34class 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
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}
$wgGitRepositoryViewers
Map GIT repository URLs to viewer URLs to provide links in Special:Version.
$wgGitInfoCacheDirectory
Directory where GitInfo will look for pre-computed cache files.
$wgGitBin
Fully specified path to git binary.
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.
$IP
Definition WebStart.php:49
@newable
Definition GitInfo.php:34
static repo()
Get the singleton for the repo at $IP.
Definition GitInfo.php:151
getHead()
Get the HEAD of the repo (without any opening "ref: ")
Definition GitInfo.php:174
static getCacheFilePath( $repoDir)
Compute the path to the cache file for a given directory.
Definition GitInfo.php:117
static array false $viewers
Map of repo URLs to viewer URLs.
Definition GitInfo.php:64
$cacheFile
Path to JSON cache file for pre-computed git information.
Definition GitInfo.php:54
$basedir
Location of the .git directory.
Definition GitInfo.php:44
getRemoteUrl()
Get the URL of the remote origin.
Definition GitInfo.php:315
static headSHA1()
Definition GitInfo.php:402
getHeadCommitDate()
Get the commit date of HEAD entry of the git code repository.
Definition GitInfo.php:230
static headViewUrl()
Definition GitInfo.php:418
precomputeValues()
Precompute and cache git information.
Definition GitInfo.php:371
$cache
Cached git information.
Definition GitInfo.php:59
cacheIsComplete()
Check to see if the current cache is fully populated.
Definition GitInfo.php:354
static $repo
Singleton for the repo at $IP.
Definition GitInfo.php:39
$repoDir
Location of the repository.
Definition GitInfo.php:49
static currentBranch()
Definition GitInfo.php:410
getHeadViewUrl()
Get an URL to a web viewer link to the HEAD revision.
Definition GitInfo.php:289
__construct( $repoDir, $usePrecomputed=true)
Definition GitInfo.php:72
static isSHA1( $str)
Check if a string looks like a hex encoded SHA1 hash.
Definition GitInfo.php:165
getHeadSHA1()
Get the SHA1 for the current HEAD of the repo.
Definition GitInfo.php:198
getCurrentBranch()
Get the name of the current branch, or HEAD if not found.
Definition GitInfo.php:271
static getViewers()
Gets the list of repository viewers.
Definition GitInfo.php:426
MediaWiki exception.
Executes shell commands.
Definition Shell.php:45