30use Psr\Log\LoggerInterface;
67 private static $viewers =
false;
70 private const CONSTRUCTOR_OPTIONS = [
77 private LoggerInterface $logger;
91 self::CONSTRUCTOR_OPTIONS, $services->getMainConfig()
93 $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
95 $this->cacheFile = $this->getCacheFilePath(
$repoDir );
96 $this->logger = LoggerFactory::getInstance(
'gitinfo' );
98 "Candidate cacheFile={$this->cacheFile} for {$repoDir}"
100 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
101 if ( $usePrecomputed &&
102 $this->cacheFile !==
null &&
103 is_readable( $this->cacheFile )
105 $this->cache = FormatJson::decode(
106 file_get_contents( $this->cacheFile ),
109 $this->logger->debug(
"Loaded git data from cache for {$repoDir}" );
113 $this->logger->debug(
"Cache incomplete for {$repoDir}" );
114 $this->basedir =
$repoDir . DIRECTORY_SEPARATOR .
'.git';
115 if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
116 $GITfile = file_get_contents( $this->basedir );
117 if ( strlen( $GITfile ) > 8 &&
118 substr( $GITfile, 0, 8 ) ===
'gitdir: '
120 $path = rtrim( substr( $GITfile, 8 ),
"\r\n" );
121 if (
$path[0] ===
'/' || substr(
$path, 1, 1 ) ===
':' ) {
123 $this->basedir =
$path;
140 private function getCacheFilePath(
$repoDir ) {
142 if ( $gitInfoCacheDirectory ===
false ) {
145 if ( $gitInfoCacheDirectory ) {
148 if ( $repoName ===
false ) {
152 $realIP = realpath( MW_INSTALL_PATH );
153 if ( str_starts_with( $repoName, $realIP ) ) {
155 $repoName = substr( $repoName, strlen( $realIP ) );
159 $repoName = strtr( $repoName, [
'/' =>
'-' ] );
160 $repoName = strtr( $repoName, [ DIRECTORY_SEPARATOR =>
'-' ] );
161 $fileName =
'info' . $repoName .
'.json';
162 $cachePath =
"{$gitInfoCacheDirectory}/{$fileName}";
163 if ( is_readable( $cachePath ) ) {
168 return "$repoDir/gitinfo.json";
176 public static function repo() {
177 if ( self::$repo ===
null ) {
178 self::$repo =
new self( MW_INSTALL_PATH );
190 return (
bool)preg_match(
'/^[0-9A-F]{40}$/i', $str );
199 if ( !isset( $this->cache[
'head'] ) ) {
200 $headFile =
"{$this->basedir}/HEAD";
203 if ( is_readable( $headFile ) ) {
204 $head = file_get_contents( $headFile );
206 if ( preg_match(
"/ref: (.*)/", $head, $m ) ) {
207 $head = rtrim( $m[1] );
209 $head = rtrim( $head );
212 $this->cache[
'head'] = $head;
214 return $this->cache[
'head'];
223 if ( !isset( $this->cache[
'headSHA1'] ) ) {
228 if ( self::isSHA1( $head ) ) {
232 $refFile =
"{$this->basedir}/{$head}";
233 $packedRefs =
"{$this->basedir}/packed-refs";
234 $headRegex = preg_quote( $head,
'/' );
235 if ( is_readable( $refFile ) ) {
236 $sha1 = rtrim( file_get_contents( $refFile ) );
237 } elseif ( is_readable( $packedRefs ) &&
238 preg_match(
"/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ),
$matches )
243 $this->cache[
'headSHA1'] = $sha1;
245 return $this->cache[
'headSHA1'];
257 if ( !isset( $this->cache[
'headCommitDate'] ) ) {
262 $isFile = @is_file( $gitBin );
264 is_executable( $gitBin ) &&
265 !Shell::isDisabled() &&
272 '--format=format:%ct',
275 $gitDir = realpath( $this->basedir );
276 $result = Shell::command( $cmd )
277 ->environment( [
'GIT_DIR' => $gitDir ] )
278 ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
279 ->allowPath( $gitDir, $this->repoDir )
282 if ( $result->getExitCode() === 0 ) {
283 $date = (int)$result->getStdout();
286 $this->cache[
'headCommitDate'] = $date;
288 return $this->cache[
'headCommitDate'];
297 if ( !isset( $this->cache[
'branch'] ) ) {
300 preg_match(
"#^refs/heads/(.*)$#", $branch, $m )
304 $this->cache[
'branch'] = $branch;
306 return $this->cache[
'branch'];
316 if (
$url ===
false ) {
319 foreach ( $this->getViewers() as
$repo => $viewer ) {
320 $pattern =
'#^' .
$repo .
'$#';
322 $viewerUrl = preg_replace( $pattern, $viewer,
$url );
325 '%h' => substr( $headSHA1, 0, 7 ),
330 return strtr( $viewerUrl, $replacements );
341 if ( !isset( $this->cache[
'remoteURL'] ) ) {
342 $config =
"{$this->basedir}/config";
344 if ( is_readable( $config ) ) {
346 $configArray = @parse_ini_file( $config,
true );
350 if ( isset( $configArray[
'remote origin'] ) ) {
351 $remote = $configArray[
'remote origin'];
352 } elseif ( is_array( $configArray ) ) {
353 foreach ( $configArray as $sectionName => $sectionConf ) {
354 if ( str_starts_with( $sectionName,
'remote' ) ) {
355 $remote = $sectionConf;
360 if ( $remote !==
false && isset( $remote[
'url'] ) ) {
361 $url = $remote[
'url'];
364 $this->cache[
'remoteURL'] =
$url;
366 return $this->cache[
'remoteURL'];
379 return isset( $this->cache[
'head'] ) &&
380 isset( $this->cache[
'headSHA1'] ) &&
381 isset( $this->cache[
'headCommitDate'] ) &&
382 isset( $this->cache[
'branch'] ) &&
383 isset( $this->cache[
'remoteURL'] );
396 if ( $this->cacheFile !==
null ) {
405 $this->logger->debug(
406 "Failed to compute GitInfo for \"{$this->basedir}\""
411 $cacheDir = dirname( $this->cacheFile );
412 if ( !file_exists( $cacheDir ) &&
415 throw new RuntimeException(
"Unable to create GitInfo cache \"{$cacheDir}\"" );
418 file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
450 private function getViewers() {
451 if ( self::$viewers ===
false ) {
453 $this->hookRunner->onGitViewers( self::$viewers );
456 return self::$viewers;
461class_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 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()