35use Psr\Log\LoggerInterface;
37use Wikimedia\AtEase\AtEase;
74 private static $viewers =
false;
77 private const CONSTRUCTOR_OPTIONS = [
104 self::CONSTRUCTOR_OPTIONS, $services->getMainConfig()
106 $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
108 $this->cacheFile = $this->getCacheFilePath(
$repoDir );
109 $this->logger = LoggerFactory::getInstance(
'gitinfo' );
110 $this->logger->debug(
111 "Candidate cacheFile={$this->cacheFile} for {$repoDir}"
113 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
114 if ( $usePrecomputed &&
115 $this->cacheFile !==
null &&
116 is_readable( $this->cacheFile )
118 $this->cache = FormatJson::decode(
119 file_get_contents( $this->cacheFile ),
122 $this->logger->debug(
"Loaded git data from cache for {$repoDir}" );
126 $this->logger->debug(
"Cache incomplete for {$repoDir}" );
127 $this->basedir =
$repoDir . DIRECTORY_SEPARATOR .
'.git';
128 if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
129 $GITfile = file_get_contents( $this->basedir );
130 if ( strlen( $GITfile ) > 8 &&
131 substr( $GITfile, 0, 8 ) ===
'gitdir: '
133 $path = rtrim( substr( $GITfile, 8 ),
"\r\n" );
134 if (
$path[0] ===
'/' || substr(
$path, 1, 1 ) ===
':' ) {
136 $this->basedir =
$path;
153 private function getCacheFilePath(
$repoDir ) {
155 if ( $gitInfoCacheDirectory ===
false ) {
159 if ( $gitInfoCacheDirectory ) {
162 $realIP = realpath( $baseDir );
164 if ( $repoName ===
false ) {
168 if ( strpos( $repoName, $realIP ) === 0 ) {
170 $repoName = substr( $repoName, strlen( $realIP ) );
174 $repoName = strtr( $repoName, DIRECTORY_SEPARATOR,
'-' );
175 $fileName =
'info' . $repoName .
'.json';
176 $cachePath =
"{$gitInfoCacheDirectory}/{$fileName}";
177 if ( is_readable( $cachePath ) ) {
182 return "$repoDir/gitinfo.json";
190 public static function repo() {
191 if ( self::$repo ===
null ) {
192 self::$repo =
new self( MW_INSTALL_PATH );
204 return (
bool)preg_match(
'/^[0-9A-F]{40}$/i', $str );
213 if ( !isset( $this->cache[
'head'] ) ) {
214 $headFile =
"{$this->basedir}/HEAD";
217 if ( is_readable( $headFile ) ) {
218 $head = file_get_contents( $headFile );
220 if ( preg_match(
"/ref: (.*)/", $head, $m ) ) {
221 $head = rtrim( $m[1] );
223 $head = rtrim( $head );
226 $this->cache[
'head'] = $head;
228 return $this->cache[
'head'];
237 if ( !isset( $this->cache[
'headSHA1'] ) ) {
242 if ( self::isSHA1( $head ) ) {
246 $refFile =
"{$this->basedir}/{$head}";
247 $packedRefs =
"{$this->basedir}/packed-refs";
248 $headRegex = preg_quote( $head,
'/' );
249 if ( is_readable( $refFile ) ) {
250 $sha1 = rtrim( file_get_contents( $refFile ) );
251 } elseif ( is_readable( $packedRefs ) &&
252 preg_match(
"/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ),
$matches )
257 $this->cache[
'headSHA1'] = $sha1;
259 return $this->cache[
'headSHA1'];
271 if ( !isset( $this->cache[
'headCommitDate'] ) ) {
275 $isFile = AtEase::quietCall(
'is_file', $gitBin );
277 is_executable( $gitBin ) &&
278 !Shell::isDisabled() &&
285 '--format=format:%ct',
288 $gitDir = realpath( $this->basedir );
289 $result = Shell::command( $cmd )
290 ->environment( [
'GIT_DIR' => $gitDir ] )
291 ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
292 ->allowPath( $gitDir, $this->repoDir )
295 if ( $result->getExitCode() === 0 ) {
296 $date = (int)$result->getStdout();
299 $this->cache[
'headCommitDate'] = $date;
301 return $this->cache[
'headCommitDate'];
310 if ( !isset( $this->cache[
'branch'] ) ) {
313 preg_match(
"#^refs/heads/(.*)$#", $branch, $m )
317 $this->cache[
'branch'] = $branch;
319 return $this->cache[
'branch'];
329 if ( $url ===
false ) {
332 foreach ( $this->getViewers() as
$repo => $viewer ) {
333 $pattern =
'#^' .
$repo .
'$#';
334 if ( preg_match( $pattern, $url,
$matches ) ) {
335 $viewerUrl = preg_replace( $pattern, $viewer, $url );
338 '%h' => substr( $headSHA1, 0, 7 ),
343 return strtr( $viewerUrl, $replacements );
354 if ( !isset( $this->cache[
'remoteURL'] ) ) {
355 $config =
"{$this->basedir}/config";
357 if ( is_readable( $config ) ) {
358 AtEase::suppressWarnings();
359 $configArray = parse_ini_file( $config,
true );
360 AtEase::restoreWarnings();
364 if ( isset( $configArray[
'remote origin'] ) ) {
365 $remote = $configArray[
'remote origin'];
366 } elseif ( is_array( $configArray ) ) {
367 foreach ( $configArray as $sectionName => $sectionConf ) {
368 if ( substr( $sectionName, 0, 6 ) ==
'remote' ) {
369 $remote = $sectionConf;
374 if ( $remote !==
false && isset( $remote[
'url'] ) ) {
375 $url = $remote[
'url'];
378 $this->cache[
'remoteURL'] = $url;
380 return $this->cache[
'remoteURL'];
393 return isset( $this->cache[
'head'] ) &&
394 isset( $this->cache[
'headSHA1'] ) &&
395 isset( $this->cache[
'headCommitDate'] ) &&
396 isset( $this->cache[
'branch'] ) &&
397 isset( $this->cache[
'remoteURL'] );
410 if ( $this->cacheFile !==
null ) {
419 $this->logger->debug(
420 "Failed to compute GitInfo for \"{$this->basedir}\""
425 $cacheDir = dirname( $this->cacheFile );
426 if ( !file_exists( $cacheDir ) &&
429 throw new RuntimeException(
"Unable to create GitInfo cache \"{$cacheDir}\"" );
432 file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
464 private function getViewers() {
465 if ( self::$viewers ===
false ) {
467 $this->hookRunner->onGitViewers( self::$viewers );
470 return self::$viewers;
477class_alias( GitInfo::class,
'GitInfo' );
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
A class containing constants representing the names of configuration variables.
const CacheDirectory
Name constant for the CacheDirectory setting, for use with Config::get()
const BaseDirectory
Name constant for the BaseDirectory setting, for use with Config::get()
const GitRepositoryViewers
Name constant for the GitRepositoryViewers setting, for use with Config::get()
const GitBin
Name constant for the GitBin setting, for use with Config::get()
const GitInfoCacheDirectory
Name constant for the GitInfoCacheDirectory setting, for use with Config::get()