]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/GitInfo.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / includes / GitInfo.php
1 <?php
2 /**
3  * A class to help return information about a git repo MediaWiki may be inside
4  * This is used by Special:Version and is also useful for the LocalSettings.php
5  * of anyone working on large branches in git to setup config that show up only
6  * when specific branches are currently checked out.
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with this program; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21  * http://www.gnu.org/copyleft/gpl.html
22  *
23  * @file
24  */
25
26 class GitInfo {
27
28         /**
29          * Singleton for the repo at $IP
30          */
31         protected static $repo = null;
32
33         /**
34          * Location of the .git directory
35          */
36         protected $basedir;
37
38         /**
39          * Path to JSON cache file for pre-computed git information.
40          */
41         protected $cacheFile;
42
43         /**
44          * Cached git information.
45          */
46         protected $cache = [];
47
48         /**
49          * @var array|false Map of repo URLs to viewer URLs. Access via static method getViewers().
50          */
51         private static $viewers = false;
52
53         /**
54          * @param string $repoDir The root directory of the repo where .git can be found
55          * @param bool $usePrecomputed Use precomputed information if available
56          * @see precomputeValues
57          */
58         public function __construct( $repoDir, $usePrecomputed = true ) {
59                 $this->cacheFile = self::getCacheFilePath( $repoDir );
60                 wfDebugLog( 'gitinfo',
61                         "Computed cacheFile={$this->cacheFile} for {$repoDir}"
62                 );
63                 if ( $usePrecomputed &&
64                         $this->cacheFile !== null &&
65                         is_readable( $this->cacheFile )
66                 ) {
67                         $this->cache = FormatJson::decode(
68                                 file_get_contents( $this->cacheFile ),
69                                 true
70                         );
71                         wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
72                 }
73
74                 if ( !$this->cacheIsComplete() ) {
75                         wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
76                         $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
77                         if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
78                                 $GITfile = file_get_contents( $this->basedir );
79                                 if ( strlen( $GITfile ) > 8 &&
80                                         substr( $GITfile, 0, 8 ) === 'gitdir: '
81                                 ) {
82                                         $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
83                                         if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
84                                                 // Path from GITfile is absolute
85                                                 $this->basedir = $path;
86                                         } else {
87                                                 $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
88                                         }
89                                 }
90                         }
91                 }
92         }
93
94         /**
95          * Compute the path to the cache file for a given directory.
96          *
97          * @param string $repoDir The root directory of the repo where .git can be found
98          * @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or
99          * fallback in the extension directory itself
100          * @since 1.24
101          */
102         protected static function getCacheFilePath( $repoDir ) {
103                 global $IP, $wgGitInfoCacheDirectory;
104
105                 if ( $wgGitInfoCacheDirectory ) {
106                         // Convert both $IP and $repoDir to canonical paths to protect against
107                         // $IP having changed between the settings files and runtime.
108                         $realIP = realpath( $IP );
109                         $repoName = realpath( $repoDir );
110                         if ( $repoName === false ) {
111                                 // Unit tests use fake path names
112                                 $repoName = $repoDir;
113                         }
114                         if ( strpos( $repoName, $realIP ) === 0 ) {
115                                 // Strip $IP from path
116                                 $repoName = substr( $repoName, strlen( $realIP ) );
117                         }
118                         // Transform path to git repo to something we can safely embed in
119                         // a filename
120                         $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
121                         $fileName = 'info' . $repoName . '.json';
122                         $cachePath = "{$wgGitInfoCacheDirectory}/{$fileName}";
123                         if ( is_readable( $cachePath ) ) {
124                                 return $cachePath;
125                         }
126                 }
127
128                 return "$repoDir/gitinfo.json";
129         }
130
131         /**
132          * Get the singleton for the repo at $IP
133          *
134          * @return GitInfo
135          */
136         public static function repo() {
137                 if ( is_null( self::$repo ) ) {
138                         global $IP;
139                         self::$repo = new self( $IP );
140                 }
141                 return self::$repo;
142         }
143
144         /**
145          * Check if a string looks like a hex encoded SHA1 hash
146          *
147          * @param string $str The string to check
148          * @return bool Whether or not the string looks like a SHA1
149          */
150         public static function isSHA1( $str ) {
151                 return !!preg_match( '/^[0-9A-F]{40}$/i', $str );
152         }
153
154         /**
155          * Get the HEAD of the repo (without any opening "ref: ")
156          *
157          * @return string|bool The HEAD (git reference or SHA1) or false
158          */
159         public function getHead() {
160                 if ( !isset( $this->cache['head'] ) ) {
161                         $headFile = "{$this->basedir}/HEAD";
162                         $head = false;
163
164                         if ( is_readable( $headFile ) ) {
165                                 $head = file_get_contents( $headFile );
166
167                                 if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
168                                         $head = rtrim( $m[1] );
169                                 } else {
170                                         $head = rtrim( $head );
171                                 }
172                         }
173                         $this->cache['head'] = $head;
174                 }
175                 return $this->cache['head'];
176         }
177
178         /**
179          * Get the SHA1 for the current HEAD of the repo
180          *
181          * @return string|bool A SHA1 or false
182          */
183         public function getHeadSHA1() {
184                 if ( !isset( $this->cache['headSHA1'] ) ) {
185                         $head = $this->getHead();
186                         $sha1 = false;
187
188                         // If detached HEAD may be a SHA1
189                         if ( self::isSHA1( $head ) ) {
190                                 $sha1 = $head;
191                         } else {
192                                 // If not a SHA1 it may be a ref:
193                                 $refFile = "{$this->basedir}/{$head}";
194                                 if ( is_readable( $refFile ) ) {
195                                         $sha1 = rtrim( file_get_contents( $refFile ) );
196                                 }
197                         }
198                         $this->cache['headSHA1'] = $sha1;
199                 }
200                 return $this->cache['headSHA1'];
201         }
202
203         /**
204          * Get the commit date of HEAD entry of the git code repository
205          *
206          * @since 1.22
207          * @return int|bool Commit date (UNIX timestamp) or false
208          */
209         public function getHeadCommitDate() {
210                 global $wgGitBin;
211
212                 if ( !isset( $this->cache['headCommitDate'] ) ) {
213                         $date = false;
214                         if ( is_file( $wgGitBin ) &&
215                                 is_executable( $wgGitBin ) &&
216                                 $this->getHead() !== false
217                         ) {
218                                 $environment = [ "GIT_DIR" => $this->basedir ];
219                                 $cmd = wfEscapeShellArg( $wgGitBin ) .
220                                         " show -s --format=format:%ct HEAD";
221                                 $retc = false;
222                                 $commitDate = wfShellExec( $cmd, $retc, $environment );
223                                 if ( $retc === 0 ) {
224                                         $date = (int)$commitDate;
225                                 }
226                         }
227                         $this->cache['headCommitDate'] = $date;
228                 }
229                 return $this->cache['headCommitDate'];
230         }
231
232         /**
233          * Get the name of the current branch, or HEAD if not found
234          *
235          * @return string|bool The branch name, HEAD, or false
236          */
237         public function getCurrentBranch() {
238                 if ( !isset( $this->cache['branch'] ) ) {
239                         $branch = $this->getHead();
240                         if ( $branch &&
241                                 preg_match( "#^refs/heads/(.*)$#", $branch, $m )
242                         ) {
243                                 $branch = $m[1];
244                         }
245                         $this->cache['branch'] = $branch;
246                 }
247                 return $this->cache['branch'];
248         }
249
250         /**
251          * Get an URL to a web viewer link to the HEAD revision.
252          *
253          * @return string|bool String if a URL is available or false otherwise
254          */
255         public function getHeadViewUrl() {
256                 $url = $this->getRemoteUrl();
257                 if ( $url === false ) {
258                         return false;
259                 }
260                 foreach ( self::getViewers() as $repo => $viewer ) {
261                         $pattern = '#^' . $repo . '$#';
262                         if ( preg_match( $pattern, $url, $matches ) ) {
263                                 $viewerUrl = preg_replace( $pattern, $viewer, $url );
264                                 $headSHA1 = $this->getHeadSHA1();
265                                 $replacements = [
266                                         '%h' => substr( $headSHA1, 0, 7 ),
267                                         '%H' => $headSHA1,
268                                         '%r' => urlencode( $matches[1] ),
269                                         '%R' => $matches[1],
270                                 ];
271                                 return strtr( $viewerUrl, $replacements );
272                         }
273                 }
274                 return false;
275         }
276
277         /**
278          * Get the URL of the remote origin.
279          * @return string|bool String if a URL is available or false otherwise.
280          */
281         protected function getRemoteUrl() {
282                 if ( !isset( $this->cache['remoteURL'] ) ) {
283                         $config = "{$this->basedir}/config";
284                         $url = false;
285                         if ( is_readable( $config ) ) {
286                                 MediaWiki\suppressWarnings();
287                                 $configArray = parse_ini_file( $config, true );
288                                 MediaWiki\restoreWarnings();
289                                 $remote = false;
290
291                                 // Use the "origin" remote repo if available or any other repo if not.
292                                 if ( isset( $configArray['remote origin'] ) ) {
293                                         $remote = $configArray['remote origin'];
294                                 } elseif ( is_array( $configArray ) ) {
295                                         foreach ( $configArray as $sectionName => $sectionConf ) {
296                                                 if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
297                                                         $remote = $sectionConf;
298                                                 }
299                                         }
300                                 }
301
302                                 if ( $remote !== false && isset( $remote['url'] ) ) {
303                                         $url = $remote['url'];
304                                 }
305                         }
306                         $this->cache['remoteURL'] = $url;
307                 }
308                 return $this->cache['remoteURL'];
309         }
310
311         /**
312          * Check to see if the current cache is fully populated.
313          *
314          * Note: This method is public only to make unit testing easier. There's
315          * really no strong reason that anything other than a test should want to
316          * call this method.
317          *
318          * @return bool True if all expected cache keys exist, false otherwise
319          */
320         public function cacheIsComplete() {
321                 return isset( $this->cache['head'] ) &&
322                         isset( $this->cache['headSHA1'] ) &&
323                         isset( $this->cache['headCommitDate'] ) &&
324                         isset( $this->cache['branch'] ) &&
325                         isset( $this->cache['remoteURL'] );
326         }
327
328         /**
329          * Precompute and cache git information.
330          *
331          * Creates a JSON file in the cache directory associated with this
332          * GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing
333          * the same directory to avoid needing to examine the .git directory again.
334          *
335          * @since 1.24
336          */
337         public function precomputeValues() {
338                 if ( $this->cacheFile !== null ) {
339                         // Try to completely populate the cache
340                         $this->getHead();
341                         $this->getHeadSHA1();
342                         $this->getHeadCommitDate();
343                         $this->getCurrentBranch();
344                         $this->getRemoteUrl();
345
346                         if ( !$this->cacheIsComplete() ) {
347                                 wfDebugLog( 'gitinfo',
348                                         "Failed to compute GitInfo for \"{$this->basedir}\""
349                                 );
350                                 return;
351                         }
352
353                         $cacheDir = dirname( $this->cacheFile );
354                         if ( !file_exists( $cacheDir ) &&
355                                 !wfMkdirParents( $cacheDir, null, __METHOD__ )
356                         ) {
357                                 throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
358                         }
359
360                         file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
361                 }
362         }
363
364         /**
365          * @see self::getHeadSHA1
366          * @return string
367          */
368         public static function headSHA1() {
369                 return self::repo()->getHeadSHA1();
370         }
371
372         /**
373          * @see self::getCurrentBranch
374          * @return string
375          */
376         public static function currentBranch() {
377                 return self::repo()->getCurrentBranch();
378         }
379
380         /**
381          * @see self::getHeadViewUrl()
382          * @return bool|string
383          */
384         public static function headViewUrl() {
385                 return self::repo()->getHeadViewUrl();
386         }
387
388         /**
389          * Gets the list of repository viewers
390          * @return array
391          */
392         protected static function getViewers() {
393                 global $wgGitRepositoryViewers;
394
395                 if ( self::$viewers === false ) {
396                         self::$viewers = $wgGitRepositoryViewers;
397                         Hooks::run( 'GitViewers', [ &self::$viewers ] );
398                 }
399
400                 return self::$viewers;
401         }
402 }