44$ opts = parseOpts ();
55
66$ changes = array ();
7+ $ data_from = load ($ opts ['from ' ], $ opts ['path ' ], $ opts ['vcs ' ], '' );
8+ $ data_to = load ($ opts ['to ' ], $ opts ['path ' ], $ opts ['vcs ' ], 'composer.lock ' );
79
810if (! $ opts ['only-dev ' ]) {
9- $ changes ['changes ' ] = diff ('packages ' , $ opts [ ' from ' ] , $ opts [ ' to ' ], $ opts [ ' path ' ] );
11+ $ changes ['changes ' ] = diff ('packages ' , $ data_from , $ data_to );
1012}
1113
1214if (! $ opts ['only-prod ' ]) {
13- $ changes ['changes-dev ' ] = diff ('packages-dev ' , $ opts [ ' from ' ] , $ opts [ ' to ' ], $ opts [ ' path ' ] );
15+ $ changes ['changes-dev ' ] = diff ('packages-dev ' , $ data_from , $ data_to );
1416}
1517
1618if ($ opts ['json ' ]) {
@@ -31,28 +33,24 @@ if ($opts['md']) {
3133 ));
3234}
3335
34- $ table_titles = [
36+ $ table_titles = array (
3537 'changes ' => 'Production Changes ' ,
3638 'changes-dev ' => 'Dev Changes ' ,
37- ] ;
39+ ) ;
3840
3941foreach ($ changes as $ k => $ diff ) {
4042 print tableize ($ table_titles [$ k ], $ diff , $ table_opts );
4143}
4244
43- function diff ($ key , $ from , $ to , $ base_path ) {
45+ function diff ($ key , $ data_from , $ data_to ) {
4446
4547 $ pkgs = array ();
4648
47- $ data = load ($ from , $ base_path );
48-
49- foreach ($ data ->$ key as $ pkg ) {
49+ foreach ($ data_from ->$ key as $ pkg ) {
5050 $ pkgs [$ pkg ->name ] = array (version ($ pkg ), 'REMOVED ' , '' );
5151 }
5252
53- $ data = load ($ to , $ base_path );
54-
55- foreach ($ data ->$ key as $ pkg ) {
53+ foreach ($ data_to ->$ key as $ pkg ) {
5654 if (! array_key_exists ($ pkg ->name , $ pkgs )) {
5755 $ pkgs [$ pkg ->name ] = array ('NEW ' , version ($ pkg ), '' );
5856 continue ;
@@ -150,46 +148,72 @@ function urlFormatterMd($url, $text) {
150148 return sprintf ('[%s](%s) ' , $ text , $ url );
151149}
152150
153- function load ($ fileish , $ base_path = '' ) {
154- $ orig = $ fileish ;
155-
156- if (empty ($ base_path )) {
157- $ base_path = '. ' . DIRECTORY_SEPARATOR ;
158- } else {
159- $ base_path = rtrim ($ base_path , DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR ;
151+ // $fileish is what the user actually requested.
152+ // $default_fileish is what it should be if $fileish is empty
153+ function load ($ fileish , $ base_path , $ force_vcs , $ default_fileish ) {
154+ $ loaders = ($ force_vcs || (empty ($ fileish ) && empty ($ default_fileish ))) ? array () : array ('loadFile ' );
155+
156+ $ vcses = $ force_vcs ? array ($ force_vcs ) : getVcses ();
157+ $ errors = array ();
158+
159+ foreach ($ vcses as $ vcs ) {
160+ $ detector = 'vcsDetect ' . ucfirst ($ vcs );
161+ if ($ vcs != $ force_vcs && function_exists ($ detector )) {
162+ list ($ available , $ err ) = call_user_func ($ detector , $ fileish , $ base_path , $ default_fileish );
163+ if ($ err ) {
164+ $ errors [] = $ err ;
165+ continue ;
166+ }
167+ if (!$ available ) continue ;
168+ }
169+ $ loaders [] = 'vcsLoad ' . ucfirst ($ vcs );
160170 }
161171
162- if (empty ($ fileish )) {
163- $ fileish = $ base_path . 'composer.lock ' ;
172+ if (empty ($ loaders )) {
173+ error_log (implode ("\n" , $ errors ));
174+ if ($ force_vcs ) {
175+ error_log ("Requested vcs ' $ force_vcs' not installed or otherwise unavailable " );
176+ } else {
177+ error_log ("No loaders were found; perhaps your vcs cli tools are not installed, not in PATH, or otherwise unavailable " );
178+ }
179+ exit (1 );
164180 }
165181
166- if (isUrl ($ fileish )) {
167- if (! in_array (parse_url ($ fileish , PHP_URL_SCHEME ), stream_get_wrappers ())) {
168- error_log ("Error: no stream wrapper to open ' $ fileish' " );
169- exit (1 );
182+ $ errors = array ();
183+ foreach ($ loaders as $ loader ) {
184+ list ($ result , $ err ) = call_user_func_array ($ loader , array ($ fileish , $ base_path , $ default_fileish ));
185+ if (empty ($ err )) {
186+ return $ result ;
170187 }
171-
172- return mustDecodeJson (file_get_contents ($ fileish ), $ fileish );
188+ $ errors [] = "Failed to find ' $ fileish' with ' $ loader'; $ err " ;
173189 }
174190
175- if ( file_exists ( $ fileish ) ) {
176- return mustDecodeJson ( file_get_contents ( $ fileish ), $ fileish );
191+ foreach ( $ errors as $ e ) {
192+ error_log ( $ e );
177193 }
178194
179- if (strpos ($ orig , ': ' ) === false ) {
180- $ fileish .= ': ' . $ base_path . 'composer.lock ' ;
181- }
195+ exit (1 );
196+ }
182197
183- $ lines = array ();
198+ function loadFile ($ fileish , $ base_path , $ default_fileish ) {
199+ if (empty ($ fileish )) {
200+ $ fileish = $ default_fileish ;
201+ if (!empty ($ base_path )) {
202+ $ fileish = joinPath ($ base_path , $ fileish );
203+ }
204+ }
184205
185- exec ('git show ' . escapeshellarg ($ fileish ), $ lines , $ exit );
206+ // Does it look like a url that we can handle with stream wrappers?
207+ if (isUrl ($ fileish ) && in_array (parse_url ($ fileish , PHP_URL_SCHEME ), stream_get_wrappers ())) {
208+ return array (mustDecodeJson (file_get_contents ($ fileish ), $ fileish ), false );
209+ }
186210
187- if ( $ exit !== 0 ) {
188- error_log ( " Error: cannot open $ orig or find it in git as $ fileish" );
189- exit ( 1 );
211+ // Is it a file in the local filesystem?
212+ if ( file_exists ( $ fileish)) {
213+ return array ( mustDecodeJson ( file_get_contents ( $ fileish ), $ fileish ), false );
190214 }
191215
192- return mustDecodeJson ( implode ( "\n" , $ lines ), $ fileish );
216+ return array ( false , " Candidate ' $ fileish' does not look loadable from the fs or php stream wrappers " );
193217}
194218
195219function isUrl ($ string ) {
@@ -278,8 +302,126 @@ function formatCompareDrupal($url, $from, $to) {
278302 return sprintf ('%s/compare/8.x-%s...8.x-%s ' , $ url , substr (urlencode ($ from ), 0 , -2 ), substr (urlencode ($ to ), 0 , -2 ));
279303}
280304
305+ //
306+ // ## VCSes ####################
307+ //
308+
309+ function getVcses () {
310+ return array ('git ' , 'svn ' );
311+ }
312+
313+ function vcsDetectGit ($ _fileish ) {
314+ // Is there a git executable?
315+ exec ('sh -c "git --version" > /dev/null 2>&1 ' , $ _out , $ exit );
316+ if ($ exit !== 0 ) return array (false , "'git --version' exited with non-zero code ' $ exit' " );
317+
318+ // Does this look like a git repo?
319+ $ path = findUp ('. ' , '.git ' );
320+ return array (!! $ path , ($ path ) ? false : "Could not find .git in current directory or parents " );
321+ }
322+
323+ function vcsLoadGit ($ fileish , $ base_path , $ _default_fileish ) {
324+ // We don't care about $default_fileish here - we are expected to load from
325+ // git and we must make a filename to do that.
326+ if (empty ($ fileish )) {
327+ $ fileish = 'HEAD ' ;
328+ }
329+
330+ if (strpos ($ fileish , ': ' ) === false ) {
331+ $ fileish .= ': ' . $ base_path . 'composer.lock ' ;
332+ }
333+
334+ $ lines = array ();
335+ exec ('git show ' . escapeshellarg ($ fileish ), $ lines , $ exit );
336+
337+ if ($ exit !== 0 ) {
338+ return array ('' , "'git show $ fileish' exited with non-zero code ' $ exit' " );
339+ }
340+
341+ return array (mustDecodeJson (implode ("\n" , $ lines ), $ fileish ), false );
342+ }
343+
344+ function vcsDetectSvn ($ fileish , $ base_path , $ default_fileish ) {
345+ // Is there a git executable?
346+ exec ('sh -c "svn --version" > /dev/null 2>&1 ' , $ _out , $ exit );
347+ if ($ exit !== 0 ) return array (false , "'svn --version' exited with non-zero code ' $ exit' " );
348+
349+ if (strpos ('svn:// ' , $ fileish ) === 0 ) {
350+ return array (true , false );
351+ }
352+
353+ // Does this look like a svn repo?
354+ $ path = findUp ('. ' , '.svn ' );
355+ return array (!! $ path , ($ path ) ? false : "Could not find .svn in current directory or parents " );
356+ }
357+
358+ function vcsLoadSvn ($ fileish , $ base_path , $ _default_fileish ) {
359+ // We don't care about $default_fileish here - we are expected to load from
360+ // svn and we must make a filename to do that.
361+ if (empty ($ fileish )) {
362+ $ fileish = 'BASE ' ;
363+ }
364+
365+ // If $fileish starts with a url scheme that 'svn cat' can handle or ^, or
366+ // if it contains a @, assume it is already a proper svn identifier.
367+ // - file:// http:// https:// svn:// svn+ssh:// => absolute url of
368+ // repository (file/http/https may have been handled with stream wrappers
369+ // if '--vcs svn' wasn't specified)
370+ // - ^ => relative url from current workspace repository
371+ // - @ => repository url with revision
372+ if (preg_match ('#^\^|^(file|http|https|svn|svn\+ssh)://|@#i ' , $ fileish ) === 0 ) {
373+ $ fileish = $ base_path . 'composer.lock@ ' .$ fileish ;
374+ }
375+
376+ exec ('svn cat ' . escapeshellarg ($ fileish ), $ lines , $ exit );
377+
378+ if ($ exit !== 0 ) {
379+ return array ('' , "'svn cat $ fileish' exited with non-zero code ' $ exit' " );
380+ }
381+
382+ return array (mustDecodeJson (implode ("\n" , $ lines ), $ fileish ), false );
383+ }
384+
385+ function findUp ($ path , $ filename , $ tries = 10 ) {
386+ if (empty ($ path )) {
387+ $ path = '. ' ;
388+ }
389+
390+ // > Trailing delimiters, such as \ and /, are also removed
391+ // > returns false on failure, e.g. if the file does not exist.
392+ $ path = realpath ($ path );
393+ if ($ path === false ) return false ;
394+
395+ do {
396+ $ candidate = joinPath ($ path , $ filename );
397+
398+ if (file_exists ($ candidate )) {
399+ return $ candidate ;
400+ }
401+
402+ $ path = dirnameSafe ($ path );
403+ } while ($ path !== false && --$ tries > 0 );
404+
405+ return false ;
406+ }
407+
408+ function dirnameSafe ($ path ) {
409+ $ parent = dirname ($ path );
410+ return ($ parent != $ path && !empty ($ parent )) ? $ parent : false ;
411+ }
412+
413+ function joinPath (/* path parts */ ) {
414+ return implode (DIRECTORY_SEPARATOR , array_map (function ($ part ) {
415+ return trim ($ part , DIRECTORY_SEPARATOR );
416+ }, func_get_args ()));
417+ }
418+
419+ function ensureTrailingPathSep ($ path ) {
420+ return trim ($ path , DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR ;
421+ }
422+
281423function parseOpts () {
282- $ given = getopt ('hp: ' , array ('path: ' , 'from: ' , 'to: ' , 'md ' , 'json ' , 'pretty ' , 'no-links ' , 'only-prod ' , 'only-dev ' , 'help ' ));
424+ $ given = getopt ('hp: ' , array ('path: ' , 'from: ' , 'to: ' , 'md ' , 'json ' , 'pretty ' , 'no-links ' , 'only-prod ' , 'only-dev ' , 'help ' , ' vcs: ' ));
283425
284426 foreach (array ('help ' => 'h ' , 'path ' => 'p ' ) as $ long => $ short ) {
285427 if (array_key_exists ($ short , $ given )) {
@@ -292,38 +434,49 @@ function parseOpts() {
292434 usage ();
293435 }
294436
437+ $ vcs = array_key_exists ('vcs ' , $ given ) ? $ given ['vcs ' ] : '' ;
438+ if ($ vcs && !function_exists ('vcsLoad ' . ucfirst ($ vcs ))) {
439+ error_log ("Unsupported vcs ' $ vcs' \n" );
440+ usage ();
441+ }
442+
295443 return array (
296- 'path ' => array_key_exists ('path ' , $ given ) ? $ given ['path ' ] : '' ,
297- 'from ' => array_key_exists ('from ' , $ given ) ? $ given ['from ' ] : 'HEAD ' ,
444+ 'path ' => array_key_exists ('path ' , $ given ) ? ensureTrailingPathSep ( $ given ['path ' ]) : '' ,
445+ 'from ' => array_key_exists ('from ' , $ given ) ? $ given ['from ' ] : '' ,
298446 'to ' => array_key_exists ('to ' , $ given ) ? $ given ['to ' ] : '' ,
299447 'md ' => array_key_exists ('md ' , $ given ),
300448 'json ' => array_key_exists ('json ' , $ given ),
301449 'pretty ' => version_compare (PHP_VERSION , '5.4.0 ' , '>= ' ) && array_key_exists ('pretty ' , $ given ),
302450 'no-links ' => array_key_exists ('no-links ' , $ given ),
303451 'only-prod ' => array_key_exists ('only-prod ' , $ given ),
304452 'only-dev ' => array_key_exists ('only-dev ' , $ given ),
453+ 'vcs ' => $ vcs ,
305454 );
306455}
307456
308457function usage () {
458+ $ vcses = implode (', ' , getVcses ());
309459 print <<<EOF
310460Usage: composer-lock-diff [options]
311461
312462Options:
313463 -h --help Print this message
314464 --path, -p Base to with which to prefix paths. Default "./"
315465 E.g. `-p app` would look for HEAD:app/composer.lock and app/composer.lock
316- --from The file, git ref, or git ref with filename to compare from (HEAD:composer.lock)
466+ --from The file, git ref, or git ref with filename to compare from
467+ (git: HEAD:composer.lock, svn: composer.lock@BASE)
317468 --to The file, git ref, or git ref with filename to compare to (composer.lock)
318469 --json Format output as JSON
319470 --pretty Pretty print JSON output (PHP >= 5.4.0)
320471 --md Use markdown instead of plain text
321472 --no-links Don't include Compare links in plain text or any links in markdown
322473 --only-prod Only include changes from `packages`
323474 --only-dev Only include changes from `packages-dev`
475+ --vcs Force vcs ( $ vcses). Default: attempt to auto-detect
324476
325477EOF ;
326478
327479 exit (0 );
328480}
481+ # vim: ff=unix ts=4 ss=4 sr et
329482
0 commit comments