16use Psr\Log\LoggerInterface;
53 private static $viewers =
false;
56 private const CONSTRUCTOR_OPTIONS = [
63 private LoggerInterface $logger;
77 self::CONSTRUCTOR_OPTIONS, $services->getMainConfig()
79 $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
81 $this->cacheFile = $this->getCacheFilePath(
$repoDir );
82 $this->logger = LoggerFactory::getInstance(
'gitinfo' );
84 "Candidate cacheFile={$this->cacheFile} for {$repoDir}"
86 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
87 if ( $usePrecomputed &&
88 $this->cacheFile !==
null &&
89 is_readable( $this->cacheFile )
91 $this->cache = FormatJson::decode(
92 file_get_contents( $this->cacheFile ),
95 $this->logger->debug(
"Loaded git data from cache for {$repoDir}" );
99 $this->logger->debug(
"Cache incomplete for {$repoDir}" );
100 $this->basedir =
$repoDir . DIRECTORY_SEPARATOR .
'.git';
101 if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
102 $GITfile = file_get_contents( $this->basedir );
103 if ( strlen( $GITfile ) > 8 &&
104 substr( $GITfile, 0, 8 ) ===
'gitdir: '
106 $path = rtrim( substr( $GITfile, 8 ),
"\r\n" );
107 if (
$path[0] ===
'/' || substr(
$path, 1, 1 ) ===
':' ) {
109 $this->basedir =
$path;
126 private function getCacheFilePath(
$repoDir ) {
128 if ( $gitInfoCacheDirectory ===
false ) {
131 if ( $gitInfoCacheDirectory ) {
134 if ( $repoName ===
false ) {
138 $realIP = realpath( MW_INSTALL_PATH );
139 if ( str_starts_with( $repoName, $realIP ) ) {
141 $repoName = substr( $repoName, strlen( $realIP ) );
145 $repoName = strtr( $repoName, [
'/' =>
'-' ] );
146 $repoName = strtr( $repoName, [ DIRECTORY_SEPARATOR =>
'-' ] );
147 $fileName =
'info' . $repoName .
'.json';
148 $cachePath =
"{$gitInfoCacheDirectory}/{$fileName}";
149 if ( is_readable( $cachePath ) ) {
154 return "$repoDir/gitinfo.json";
162 public static function repo() {
163 if ( self::$repo ===
null ) {
164 self::$repo =
new self( MW_INSTALL_PATH );
176 return (
bool)preg_match(
'/^[0-9A-F]{40}$/i', $str );
185 if ( !isset( $this->cache[
'head'] ) ) {
186 $headFile =
"{$this->basedir}/HEAD";
189 if ( is_readable( $headFile ) ) {
190 $head = file_get_contents( $headFile );
192 if ( preg_match(
"/ref: (.*)/", $head, $m ) ) {
193 $head = rtrim( $m[1] );
195 $head = rtrim( $head );
198 $this->cache[
'head'] = $head;
200 return $this->cache[
'head'];
209 if ( !isset( $this->cache[
'headSHA1'] ) ) {
214 if ( self::isSHA1( $head ) ) {
218 $refFile =
"{$this->basedir}/{$head}";
219 $packedRefs =
"{$this->basedir}/packed-refs";
220 $headRegex = preg_quote( $head,
'/' );
221 if ( is_readable( $refFile ) ) {
222 $sha1 = rtrim( file_get_contents( $refFile ) );
223 } elseif ( is_readable( $packedRefs ) &&
224 preg_match(
"/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ),
$matches )
229 $this->cache[
'headSHA1'] = $sha1;
231 return $this->cache[
'headSHA1'];
243 if ( !isset( $this->cache[
'headCommitDate'] ) ) {
248 $isFile = @is_file( $gitBin );
250 is_executable( $gitBin ) &&
258 '--format=format:%ct',
261 $gitDir = realpath( $this->basedir );
262 $result = Shell::command( $cmd )
263 ->environment( [
'GIT_DIR' => $gitDir ] )
264 ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
265 ->allowPath( $gitDir, $this->repoDir )
268 if ( $result->getExitCode() === 0 ) {
269 $date = (int)$result->getStdout();
272 $this->cache[
'headCommitDate'] = $date;
274 return $this->cache[
'headCommitDate'];
283 if ( !isset( $this->cache[
'branch'] ) ) {
286 preg_match(
"#^refs/heads/(.*)$#", $branch, $m )
290 $this->cache[
'branch'] = $branch;
292 return $this->cache[
'branch'];
302 if (
$url ===
false ) {
305 foreach ( $this->getViewers() as
$repo => $viewer ) {
306 $pattern =
'#^' .
$repo .
'$#';
308 $viewerUrl = preg_replace( $pattern, $viewer,
$url );
311 '%h' => substr( $headSHA1, 0, 7 ),
316 return strtr( $viewerUrl, $replacements );
327 if ( !isset( $this->cache[
'remoteURL'] ) ) {
328 $config =
"{$this->basedir}/config";
330 if ( is_readable( $config ) ) {
332 $configArray = @parse_ini_file( $config,
true );
336 if ( isset( $configArray[
'remote origin'] ) ) {
337 $remote = $configArray[
'remote origin'];
338 } elseif ( is_array( $configArray ) ) {
339 foreach ( $configArray as $sectionName => $sectionConf ) {
340 if ( str_starts_with( $sectionName,
'remote' ) ) {
341 $remote = $sectionConf;
346 if ( $remote !==
false && isset( $remote[
'url'] ) ) {
347 $url = $remote[
'url'];
350 $this->cache[
'remoteURL'] =
$url;
352 return $this->cache[
'remoteURL'];
365 return isset( $this->cache[
'head'] ) &&
366 isset( $this->cache[
'headSHA1'] ) &&
367 isset( $this->cache[
'headCommitDate'] ) &&
368 isset( $this->cache[
'branch'] ) &&
369 isset( $this->cache[
'remoteURL'] );
382 if ( $this->cacheFile !==
null ) {
391 $this->logger->debug(
392 "Failed to compute GitInfo for \"{$this->basedir}\""
397 $cacheDir = dirname( $this->cacheFile );
398 if ( !file_exists( $cacheDir ) &&
401 throw new RuntimeException(
"Unable to create GitInfo cache \"{$cacheDir}\"" );
404 file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
436 private function getViewers() {
437 if ( self::$viewers ===
false ) {
439 $this->hookRunner->onGitViewers( self::$viewers );
442 return self::$viewers;
447class_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()