cacheFile = self::getCacheFilePath( $repoDir ); wfDebugLog( 'gitinfo', "Computed cacheFile={$this->cacheFile} for {$repoDir}" ); if ( $usePrecomputed && $this->cacheFile !== null && is_readable( $this->cacheFile ) ) { $this->cache = FormatJson::decode( file_get_contents( $this->cacheFile ), true ); wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" ); } if ( !$this->cacheIsComplete() ) { wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" ); $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git'; if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) { $GITfile = file_get_contents( $this->basedir ); if ( strlen( $GITfile ) > 8 && substr( $GITfile, 0, 8 ) === 'gitdir: ' ) { $path = rtrim( substr( $GITfile, 8 ), "\r\n" ); if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) { // Path from GITfile is absolute $this->basedir = $path; } else { $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path; } } } } } /** * Compute the path to the cache file for a given directory. * * @param string $repoDir The root directory of the repo where .git can be found * @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or * null if $wgGitInfoCacheDirectory is false (cache disabled). * @since 1.24 */ protected static function getCacheFilePath( $repoDir ) { global $IP, $wgGitInfoCacheDirectory; if ( $wgGitInfoCacheDirectory ) { // Convert both $IP and $repoDir to canonical paths to protect against // $IP having changed between the settings files and runtime. $realIP = realpath( $IP ); $repoName = realpath( $repoDir ); if ( $repoName === false ) { // Unit tests use fake path names $repoName = $repoDir; } if ( strpos( $repoName, $realIP ) === 0 ) { // Strip $IP from path $repoName = substr( $repoName, strlen( $realIP ) ); } // Transform path to git repo to something we can safely embed in // a filename $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' ); $fileName = 'info' . $repoName . '.json'; return "{$wgGitInfoCacheDirectory}/{$fileName}"; } return null; } /** * Get the singleton for the repo at $IP * * @return GitInfo */ public static function repo() { if ( is_null( self::$repo ) ) { global $IP; self::$repo = new self( $IP ); } return self::$repo; } /** * Check if a string looks like a hex encoded SHA1 hash * * @param string $str The string to check * @return bool Whether or not the string looks like a SHA1 */ public static function isSHA1( $str ) { return !!preg_match( '/^[0-9A-F]{40}$/i', $str ); } /** * Get the HEAD of the repo (without any opening "ref: ") * * @return string|bool The HEAD (git reference or SHA1) or false */ public function getHead() { if ( !isset( $this->cache['head'] ) ) { $headFile = "{$this->basedir}/HEAD"; $head = false; if ( is_readable( $headFile ) ) { $head = file_get_contents( $headFile ); if ( preg_match( "/ref: (.*)/", $head, $m ) ) { $head = rtrim( $m[1] ); } else { $head = rtrim( $head ); } } $this->cache['head'] = $head; } return $this->cache['head']; } /** * Get the SHA1 for the current HEAD of the repo * * @return string|bool A SHA1 or false */ public function getHeadSHA1() { if ( !isset( $this->cache['headSHA1'] ) ) { $head = $this->getHead(); $sha1 = false; // If detached HEAD may be a SHA1 if ( self::isSHA1( $head ) ) { $sha1 = $head; } else { // If not a SHA1 it may be a ref: $refFile = "{$this->basedir}/{$head}"; if ( is_readable( $refFile ) ) { $sha1 = rtrim( file_get_contents( $refFile ) ); } } $this->cache['headSHA1'] = $sha1; } return $this->cache['headSHA1']; } /** * Get the commit date of HEAD entry of the git code repository * * @since 1.22 * @return int|bool Commit date (UNIX timestamp) or false */ public function getHeadCommitDate() { global $wgGitBin; if ( !isset( $this->cache['headCommitDate'] ) ) { $date = false; if ( is_file( $wgGitBin ) && is_executable( $wgGitBin ) && $this->getHead() !== false ) { $environment = array( "GIT_DIR" => $this->basedir ); $cmd = wfEscapeShellArg( $wgGitBin ) . " show -s --format=format:%ct HEAD"; $retc = false; $commitDate = wfShellExec( $cmd, $retc, $environment ); if ( $retc === 0 ) { $date = (int)$commitDate; } } $this->cache['headCommitDate'] = $date; } return $this->cache['headCommitDate']; } /** * Get the name of the current branch, or HEAD if not found * * @return string|bool The branch name, HEAD, or false */ public function getCurrentBranch() { if ( !isset( $this->cache['branch'] ) ) { $branch = $this->getHead(); if ( $branch && preg_match( "#^refs/heads/(.*)$#", $branch, $m ) ) { $branch = $m[1]; } $this->cache['branch'] = $branch; } return $this->cache['branch']; } /** * Get an URL to a web viewer link to the HEAD revision. * * @return string|bool String if a URL is available or false otherwise */ public function getHeadViewUrl() { $url = $this->getRemoteUrl(); if ( $url === false ) { return false; } if ( substr( $url, -4 ) !== '.git' ) { $url .= '.git'; } foreach ( self::getViewers() as $repo => $viewer ) { $pattern = '#^' . $repo . '$#'; if ( preg_match( $pattern, $url, $matches ) ) { $viewerUrl = preg_replace( $pattern, $viewer, $url ); $headSHA1 = $this->getHeadSHA1(); $replacements = array( '%h' => substr( $headSHA1, 0, 7 ), '%H' => $headSHA1, '%r' => urlencode( $matches[1] ), ); return strtr( $viewerUrl, $replacements ); } } return false; } /** * Get the URL of the remote origin. * @return string|bool String if a URL is available or false otherwise. */ protected function getRemoteUrl() { if ( !isset( $this->cache['remoteURL'] ) ) { $config = "{$this->basedir}/config"; $url = false; if ( is_readable( $config ) ) { MediaWiki\suppressWarnings(); $configArray = parse_ini_file( $config, true ); MediaWiki\restoreWarnings(); $remote = false; // Use the "origin" remote repo if available or any other repo if not. if ( isset( $configArray['remote origin'] ) ) { $remote = $configArray['remote origin']; } elseif ( is_array( $configArray ) ) { foreach ( $configArray as $sectionName => $sectionConf ) { if ( substr( $sectionName, 0, 6 ) == 'remote' ) { $remote = $sectionConf; } } } if ( $remote !== false && isset( $remote['url'] ) ) { $url = $remote['url']; } } $this->cache['remoteURL'] = $url; } return $this->cache['remoteURL']; } /** * Check to see if the current cache is fully populated. * * Note: This method is public only to make unit testing easier. There's * really no strong reason that anything other than a test should want to * call this method. * * @return bool True if all expected cache keys exist, false otherwise */ public function cacheIsComplete() { return isset( $this->cache['head'] ) && isset( $this->cache['headSHA1'] ) && isset( $this->cache['headCommitDate'] ) && isset( $this->cache['branch'] ) && isset( $this->cache['remoteURL'] ); } /** * Precompute and cache git information. * * Creates a JSON file in the cache directory associated with this * GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing * the same directory to avoid needing to examine the .git directory again. * * @since 1.24 */ public function precomputeValues() { if ( $this->cacheFile !== null ) { // Try to completely populate the cache $this->getHead(); $this->getHeadSHA1(); $this->getHeadCommitDate(); $this->getCurrentBranch(); $this->getRemoteUrl(); if ( !$this->cacheIsComplete() ) { wfDebugLog( 'gitinfo', "Failed to compute GitInfo for \"{$this->basedir}\"" ); return; } $cacheDir = dirname( $this->cacheFile ); if ( !file_exists( $cacheDir ) && !wfMkdirParents( $cacheDir, null, __METHOD__ ) ) { throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" ); } file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) ); } } /** * @see self::getHeadSHA1 * @return string */ public static function headSHA1() { return self::repo()->getHeadSHA1(); } /** * @see self::getCurrentBranch * @return string */ public static function currentBranch() { return self::repo()->getCurrentBranch(); } /** * @see self::getHeadViewUrl() * @return bool|string */ public static function headViewUrl() { return self::repo()->getHeadViewUrl(); } /** * Gets the list of repository viewers * @return array */ protected static function getViewers() { global $wgGitRepositoryViewers; if ( self::$viewers === false ) { self::$viewers = $wgGitRepositoryViewers; Hooks::run( 'GitViewers', array( &self::$viewers ) ); } return self::$viewers; } }