Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
#!/usr/bin/env php
<?php
/**
* If composer.json references a project that has a git directory in the vendor
* folder, and that depdencency is set to track the "latest" of a branch, then
* try to validate that the lockfile-version hasn't been outpaced.
*/
class GitUtils{
/**
* @param string $path
* @return string
*/
protected static function getCurrentBranch($path = '.'){
if(is_null($path)){
$path = ".";
}
$output = array();
exec("git -C '$path' symbolic-ref --short HEAD",$output);
return trim(join(" ",$output));
}
/**
* From the current directory, looks for vendor/ subfolders which are
* git repositories.
*
* @return string[] Package names
*/
public static function findGitVendors(){
$dirs = glob("./vendor/*/*/.git",GLOB_ONLYDIR);
$dirs = array_map(function($path){
//$parts = explode("/",$path);
//return $parts[2] . "/'"
preg_match('%^\./vendor/([^/]+/[^/]+)/\.git$%', $path, $matches);
return $matches[1];
},$dirs);
return $dirs;
}
/**
* @param $branchName
* @param string $path
* @return null
*/
public static function getRefOfGitBranch($branchName, $path = '.'){
exec("git -C $path rev-parse '$branchName'",$output,$retVal);
if($retVal === 0){
return $output[0];
}else{
return null;
}
}
}
class ComposerUtils{
/**
* @param string $path Path to file
* @return mixed JSON data
* @throws Exception
*/
public static function loadJson($path){
if(!is_readable($path)){
throw new Exception("File $path cannot be read",1);
}
$str = file_get_contents($path);
try{
$obj = self::decodeJson($str);
}catch(Exception $e){
$msg = self::jsonErrorMessage($e->getCode());
throw new Exception("JSON error in $path: $msg",2,$e);
}
return $obj;
}
/**
* @param string $str
* @return mixed JSON data
* @throws Exception
*/
public static function decodeJson($str){
$in_obj = @json_decode($str);
if($in_obj === null){
$lastErr = json_last_error();
if($lastErr !== JSON_ERROR_NONE){
throw new Exception("JSON error", $lastErr);
}
}
return $in_obj;
}
/**
* @param int $code From json_last_error()
* @return string A slightly more-readable short name for the error
*/
public static function jsonErrorMessage($code){
// For now lets be lazy and just expose the PHP constant name
$constants = array(
'JSON_ERROR_DEPTH',
'JSON_ERROR_STATE_MISMATCH',
'JSON_ERROR_CTRL_CHAR',
'JSON_ERROR_SYNTAX',
'JSON_ERROR_UTF8',
'JSON_ERROR_RECURSION',
'JSON_ERROR_INF_OR_NAN',
'JSON_ERROR_UNSUPPORTED_TYPE'
);
foreach($constants as $cname){
$val = constant($cname);
if($val !== null && $val == $code){
return $cname;
}
}
return "UNKNOWN";
}
/**
* Returns direct-dependency data from composer where the dependency
* involves the latest commit to a branch.
*
* @param object $lockConf JSON
* @return array Key is package name, payload is branch-name
*/
public static function getDevDeps($lockConf){
$found = array();
foreach($lockConf->{'require'} as $name => $ver){
$matches = array();
if(preg_match('/^dev-(\S+)/',$ver,$matches)){
$found[$name] = $matches[1];
}
}
return $found;
}
/**
* @param object $lockData JSON data from lockfile
* @param string $packageName
* @return string|null
*/
public static function getLockfileSourceRef($lockData, $packageName){
foreach($lockData->packages as $pkg){
if($pkg->name == $packageName){
return $pkg->source->reference;
}
}
return null;
}
/**
* @param $newConf
* @param $argv
* @return mixed
* @throws Exception
*/
protected static function launchComposer($newConf,$argv){
/*
* We assume that the `which` command will work.
*/
$commandAlts = array(
"composer",
"composer.phar",
);
$cmd = null;
foreach($commandAlts as $alt){
if(commandExists($alt)){
$cmd = $alt;
}
}
if($cmd === null){
throw new Exception("Unable to determine composer command, tried [".join(", ",$commandAlts)."]",3);
}
array_shift($argv); // Remove haydn.php command
putenv("COMPOSER=$newConf");
passthru("$cmd ". join(" ",$argv), $retVal);
return $retVal;
}
}
if(!(file_exists('composer.json') && file_exists('composer.lock'))){
// Nothing to do
return 0;
}
try {
$conf = ComposerUtils::loadJson('composer.json');
$lock = ComposerUtils::loadJson('composer.lock');
$devDeps = ComposerUtils::getDevDeps($conf);
$gitVendors = GitUtils::findGitVendors();
$packagesToCheck = array_intersect(array_keys($devDeps), $gitVendors);
$problems = [];
foreach ($packagesToCheck as $package) {
$lockedTo = ComposerUtils::getLockfileSourceRef($lock,$package);
$branch = $devDeps[$package];
$latest = GitUtils::getRefOfGitBranch($branch,"vendor/$package");
if($lockedTo !== $latest){
$problems[$package] = [$lockedTo,$branch,$latest];
}
}
if(count($problems) > 0){
print "Your lockfile may need to be updated for package(s): \n";
echo(join("\t",["Package name","Lockfile ver","Local branch latest","Branch name"])."\n");
foreach($problems as $packageName => $details){
list($lockedTo,$branch,$latest) = $details;
echo(join("\t",[$packageName,$lockedTo,$branch,$latest])."\n");
}
return 1;
}
}catch(Exception $e){
fwrite(STDERR,$e->getMessage()."\n");
return(100 + $e->getCode());
}
return 0;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment