Merge remote-tracking branch 'naderman/ordered-transactions'
commit
d29a387bcb
|
@ -0,0 +1,215 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\DependencyResolver;
|
||||
|
||||
/**
|
||||
* Stores decisions on installing, removing or keeping packages
|
||||
*
|
||||
* @author Nils Adermann <naderman@naderman.de>
|
||||
*/
|
||||
class Decisions implements \Iterator
|
||||
{
|
||||
const DECISION_LITERAL = 0;
|
||||
const DECISION_REASON = 1;
|
||||
|
||||
protected $pool;
|
||||
protected $decisionMap;
|
||||
protected $decisionQueue = array();
|
||||
protected $decisionQueueFree = array();
|
||||
|
||||
public function __construct($pool)
|
||||
{
|
||||
$this->pool = $pool;
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
|
||||
$this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1);
|
||||
} else {
|
||||
$this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected function addDecision($literal, $level)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
$previousDecision = $this->decisionMap[$packageId];
|
||||
if ($previousDecision != 0) {
|
||||
$literalString = $this->pool->literalToString($literal);
|
||||
$package = $this->pool->literalToPackage($literal);
|
||||
throw new SolverBugException(
|
||||
"Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."."
|
||||
);
|
||||
}
|
||||
|
||||
if ($literal > 0) {
|
||||
$this->decisionMap[$packageId] = $level;
|
||||
} else {
|
||||
$this->decisionMap[$packageId] = -$level;
|
||||
}
|
||||
}
|
||||
|
||||
public function decide($literal, $level, $why, $addToFreeQueue = false)
|
||||
{
|
||||
$this->addDecision($literal, $level);
|
||||
$this->decisionQueue[] = array(
|
||||
self::DECISION_LITERAL => $literal,
|
||||
self::DECISION_REASON => $why,
|
||||
);
|
||||
|
||||
if ($addToFreeQueue) {
|
||||
$this->decisionQueueFree[count($this->decisionQueue) - 1] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function contain($literal)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
return (
|
||||
$this->decisionMap[$packageId] > 0 && $literal > 0 ||
|
||||
$this->decisionMap[$packageId] < 0 && $literal < 0
|
||||
);
|
||||
}
|
||||
|
||||
public function satisfy($literal)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
return (
|
||||
$literal > 0 && $this->decisionMap[$packageId] > 0 ||
|
||||
$literal < 0 && $this->decisionMap[$packageId] < 0
|
||||
);
|
||||
}
|
||||
|
||||
public function conflict($literal)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
return (
|
||||
($this->decisionMap[$packageId] > 0 && $literal < 0) ||
|
||||
($this->decisionMap[$packageId] < 0 && $literal > 0)
|
||||
);
|
||||
}
|
||||
|
||||
public function decided($literalOrPackageId)
|
||||
{
|
||||
return $this->decisionMap[abs($literalOrPackageId)] != 0;
|
||||
}
|
||||
|
||||
public function undecided($literalOrPackageId)
|
||||
{
|
||||
return $this->decisionMap[abs($literalOrPackageId)] == 0;
|
||||
}
|
||||
|
||||
public function decidedInstall($literalOrPackageId)
|
||||
{
|
||||
return $this->decisionMap[abs($literalOrPackageId)] > 0;
|
||||
}
|
||||
|
||||
public function decisionLevel($literalOrPackageId)
|
||||
{
|
||||
return abs($this->decisionMap[abs($literalOrPackageId)]);
|
||||
}
|
||||
|
||||
public function decisionRule($literalOrPackageId)
|
||||
{
|
||||
$packageId = abs($literalOrPackageId);
|
||||
|
||||
foreach ($this->decisionQueue as $i => $decision) {
|
||||
if ($packageId === abs($decision[self::DECISION_LITERAL])) {
|
||||
return $decision[self::DECISION_REASON];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function atOffset($queueOffset)
|
||||
{
|
||||
return $this->decisionQueue[$queueOffset];
|
||||
}
|
||||
|
||||
public function validOffset($queueOffset)
|
||||
{
|
||||
return $queueOffset >= 0 && $queueOffset < count($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function lastReason()
|
||||
{
|
||||
return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_REASON];
|
||||
}
|
||||
|
||||
public function lastLiteral()
|
||||
{
|
||||
return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_LITERAL];
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
while ($decision = array_pop($this->decisionQueue)) {
|
||||
$this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0;
|
||||
}
|
||||
|
||||
$this->decisionQueueFree = array();
|
||||
}
|
||||
|
||||
public function resetToOffset($offset)
|
||||
{
|
||||
while (count($this->decisionQueue) > $offset + 1) {
|
||||
$decision = array_pop($this->decisionQueue);
|
||||
unset($this->decisionQueueFree[count($this->decisionQueue)]);
|
||||
$this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function revertLast()
|
||||
{
|
||||
$this->decisionMap[abs($this->lastLiteral())] = 0;
|
||||
array_pop($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function getMaxOffset()
|
||||
{
|
||||
return count($this->decisionQueue) - 1;
|
||||
}
|
||||
|
||||
public function rewind()
|
||||
{
|
||||
end($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function current()
|
||||
{
|
||||
return current($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function key()
|
||||
{
|
||||
return key($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function next()
|
||||
{
|
||||
return prev($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function valid()
|
||||
{
|
||||
return false !== current($this->decisionQueue);
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
return count($this->decisionQueue) === 0;
|
||||
}
|
||||
}
|
|
@ -190,23 +190,18 @@ class RuleSetGenerator
|
|||
}
|
||||
}
|
||||
|
||||
// check implicit obsoletes
|
||||
// for installed packages we only need to check installed/installed problems,
|
||||
// as the others are picked up when looking at the uninstalled package.
|
||||
if (!$isInstalled) {
|
||||
$obsoleteProviders = $this->pool->whatProvides($package->getName(), null);
|
||||
$obsoleteProviders = $this->pool->whatProvides($package->getName(), null);
|
||||
|
||||
foreach ($obsoleteProviders as $provider) {
|
||||
if ($provider === $package) {
|
||||
continue;
|
||||
}
|
||||
foreach ($obsoleteProviders as $provider) {
|
||||
if ($provider === $package) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
|
||||
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package));
|
||||
} elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
|
||||
$reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
|
||||
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package));
|
||||
}
|
||||
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
|
||||
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package));
|
||||
} elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
|
||||
$reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
|
||||
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,16 +72,11 @@ class RuleWatchGraph
|
|||
* @param int $decidedLiteral The literal which was decided (A in our example)
|
||||
* @param int $level The level at which the decision took place and at which
|
||||
* all resulting decisions should be made.
|
||||
* @param Callable $decisionsSatisfyCallback A callback which checks if a
|
||||
* literal has already been positively decided and the rule is thus
|
||||
* already true and can be skipped.
|
||||
* @param Callable $conflictCallback A callback which checks if a literal
|
||||
* would conflict with previously made decisions on the same package
|
||||
* @param Callable $decideCallback A callback which is responsible for
|
||||
* registering decided literals resulting from unit clauses
|
||||
* @param Decisions $decisions Used to check previous decisions and to
|
||||
* register decisions resulting from propagation
|
||||
* @return Rule|null If a conflict is found the conflicting rule is returned
|
||||
*/
|
||||
public function propagateLiteral($decidedLiteral, $level, $decisionsSatisfyCallback, $conflictCallback, $decideCallback)
|
||||
public function propagateLiteral($decidedLiteral, $level, $decisions)
|
||||
{
|
||||
// we invert the decided literal here, example:
|
||||
// A was decided => (-A|B) now requires B to be true, so we look for
|
||||
|
@ -99,13 +94,13 @@ class RuleWatchGraph
|
|||
$node = $chain->current();
|
||||
$otherWatch = $node->getOtherWatch($literal);
|
||||
|
||||
if (!$node->getRule()->isDisabled() && !call_user_func($decisionsSatisfyCallback, $otherWatch)) {
|
||||
if (!$node->getRule()->isDisabled() && !$decisions->contain($otherWatch)) {
|
||||
$ruleLiterals = $node->getRule()->getLiterals();
|
||||
|
||||
$alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $conflictCallback) {
|
||||
$alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) {
|
||||
return $literal !== $ruleLiteral &&
|
||||
$otherWatch !== $ruleLiteral &&
|
||||
!call_user_func($conflictCallback, $ruleLiteral);
|
||||
!$decisions->conflict($ruleLiteral);
|
||||
});
|
||||
|
||||
if ($alternativeLiterals) {
|
||||
|
@ -114,11 +109,11 @@ class RuleWatchGraph
|
|||
continue;
|
||||
}
|
||||
|
||||
if (call_user_func($conflictCallback, $otherWatch)) {
|
||||
if ($decisions->conflict($otherWatch)) {
|
||||
return $node->getRule();
|
||||
}
|
||||
|
||||
call_user_func($decideCallback, $otherWatch, $level, $node->getRule());
|
||||
$decisions->decide($otherWatch, $level, $node->getRule());
|
||||
}
|
||||
|
||||
$chain->next();
|
||||
|
|
|
@ -47,9 +47,9 @@ class RuleWatchNode
|
|||
* Useful for learned rules where the literal for the highest rule is most
|
||||
* likely to quickly lead to further decisions.
|
||||
*
|
||||
* @param SplFixedArray $decisionMap A package to decision lookup table
|
||||
* @param Decisions $decisions The decisions made so far by the solver
|
||||
*/
|
||||
public function watch2OnHighest($decisionMap)
|
||||
public function watch2OnHighest(Decisions $decisions)
|
||||
{
|
||||
$literals = $this->rule->getLiterals();
|
||||
|
||||
|
@ -61,7 +61,7 @@ class RuleWatchNode
|
|||
$watchLevel = 0;
|
||||
|
||||
foreach ($literals as $literal) {
|
||||
$level = abs($decisionMap[abs($literal)]);
|
||||
$level = $decisions->decisionLevel($literal);
|
||||
|
||||
if ($level > $watchLevel) {
|
||||
$this->rule->watch2 = $literal;
|
||||
|
|
|
@ -29,12 +29,9 @@ class Solver
|
|||
protected $addedMap = array();
|
||||
protected $updateMap = array();
|
||||
protected $watchGraph;
|
||||
protected $decisionMap;
|
||||
protected $decisions;
|
||||
protected $installedMap;
|
||||
|
||||
protected $decisionQueue = array();
|
||||
protected $decisionQueueWhy = array();
|
||||
protected $decisionQueueFree = array();
|
||||
protected $propagateIndex;
|
||||
protected $branches = array();
|
||||
protected $problems = array();
|
||||
|
@ -48,21 +45,10 @@ class Solver
|
|||
$this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
|
||||
}
|
||||
|
||||
private function findDecisionRule($packageId)
|
||||
{
|
||||
foreach ($this->decisionQueue as $i => $literal) {
|
||||
if ($packageId === abs($literal)) {
|
||||
return $this->decisionQueueWhy[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// aka solver_makeruledecisions
|
||||
private function makeAssertionRuleDecisions()
|
||||
{
|
||||
$decisionStart = count($this->decisionQueue);
|
||||
$decisionStart = $this->decisions->getMaxOffset();
|
||||
|
||||
for ($ruleIndex = 0; $ruleIndex < count($this->rules); $ruleIndex++) {
|
||||
$rule = $this->rules->ruleById($ruleIndex);
|
||||
|
@ -74,12 +60,12 @@ class Solver
|
|||
$literals = $rule->getLiterals();
|
||||
$literal = $literals[0];
|
||||
|
||||
if (!$this->decided(abs($literal))) {
|
||||
$this->decide($literal, 1, $rule);
|
||||
if (!$this->decisions->decided(abs($literal))) {
|
||||
$this->decisions->decide($literal, 1, $rule);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->decisionsSatisfy($literal)) {
|
||||
if ($this->decisions->satisfy($literal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -89,7 +75,7 @@ class Solver
|
|||
continue;
|
||||
}
|
||||
|
||||
$conflict = $this->findDecisionRule(abs($literal));
|
||||
$conflict = $this->decisions->decisionRule($literal);
|
||||
|
||||
if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
|
||||
|
||||
|
@ -126,13 +112,7 @@ class Solver
|
|||
}
|
||||
$this->problems[] = $problem;
|
||||
|
||||
// start over
|
||||
while (count($this->decisionQueue) > $decisionStart) {
|
||||
$decisionLiteral = array_pop($this->decisionQueue);
|
||||
array_pop($this->decisionQueueWhy);
|
||||
unset($this->decisionQueueFree[count($this->decisionQueue)]);
|
||||
$this->decisionMap[abs($decisionLiteral)] = 0;
|
||||
}
|
||||
$this->resetToOffset($decisionStart);
|
||||
$ruleIndex = -1;
|
||||
}
|
||||
}
|
||||
|
@ -177,11 +157,7 @@ class Solver
|
|||
|
||||
$this->setupInstalledMap();
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
|
||||
$this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1);
|
||||
} else {
|
||||
$this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0);
|
||||
}
|
||||
$this->decisions = new Decisions($this->pool);
|
||||
|
||||
$this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap);
|
||||
$this->watchGraph = new RuleWatchGraph;
|
||||
|
@ -195,11 +171,18 @@ class Solver
|
|||
|
||||
$this->runSat(true);
|
||||
|
||||
// decide to remove everything that's installed and undecided
|
||||
foreach ($this->installedMap as $packageId => $void) {
|
||||
if ($this->decisions->undecided($packageId)) {
|
||||
$this->decisions->decide(-$packageId, 1, null);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->problems) {
|
||||
throw new SolverProblemsException($this->problems);
|
||||
}
|
||||
|
||||
$transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisionMap, $this->decisionQueue, $this->decisionQueueWhy);
|
||||
$transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
|
||||
|
||||
return $transaction->getOperations();
|
||||
}
|
||||
|
@ -211,77 +194,6 @@ class Solver
|
|||
return new Literal($package, $id > 0);
|
||||
}
|
||||
|
||||
protected function addDecision($literal, $level)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
$previousDecision = $this->decisionMap[$packageId];
|
||||
if ($previousDecision != 0) {
|
||||
$literalString = $this->pool->literalToString($literal);
|
||||
$package = $this->pool->literalToPackage($literal);
|
||||
throw new SolverBugException(
|
||||
"Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."."
|
||||
);
|
||||
}
|
||||
|
||||
if ($literal > 0) {
|
||||
$this->decisionMap[$packageId] = $level;
|
||||
} else {
|
||||
$this->decisionMap[$packageId] = -$level;
|
||||
}
|
||||
}
|
||||
|
||||
public function decide($literal, $level, $why)
|
||||
{
|
||||
$this->addDecision($literal, $level);
|
||||
$this->decisionQueue[] = $literal;
|
||||
$this->decisionQueueWhy[] = $why;
|
||||
}
|
||||
|
||||
public function decisionsContain($literal)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
return (
|
||||
$this->decisionMap[$packageId] > 0 && $literal > 0 ||
|
||||
$this->decisionMap[$packageId] < 0 && $literal < 0
|
||||
);
|
||||
}
|
||||
|
||||
protected function decisionsSatisfy($literal)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
return (
|
||||
$literal > 0 && $this->decisionMap[$packageId] > 0 ||
|
||||
$literal < 0 && $this->decisionMap[$packageId] < 0
|
||||
);
|
||||
}
|
||||
|
||||
public function decisionsConflict($literal)
|
||||
{
|
||||
$packageId = abs($literal);
|
||||
|
||||
return (
|
||||
($this->decisionMap[$packageId] > 0 && $literal < 0) ||
|
||||
($this->decisionMap[$packageId] < 0 && $literal > 0)
|
||||
);
|
||||
}
|
||||
protected function decided($packageId)
|
||||
{
|
||||
return $this->decisionMap[$packageId] != 0;
|
||||
}
|
||||
|
||||
protected function undecided($packageId)
|
||||
{
|
||||
return $this->decisionMap[$packageId] == 0;
|
||||
}
|
||||
|
||||
protected function decidedInstall($packageId)
|
||||
{
|
||||
return $this->decisionMap[$packageId] > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a decision and propagates it to all rules.
|
||||
*
|
||||
|
@ -292,15 +204,17 @@ class Solver
|
|||
*/
|
||||
protected function propagate($level)
|
||||
{
|
||||
while ($this->propagateIndex < count($this->decisionQueue)) {
|
||||
while ($this->decisions->validOffset($this->propagateIndex)) {
|
||||
$decision = $this->decisions->atOffset($this->propagateIndex);
|
||||
|
||||
$conflict = $this->watchGraph->propagateLiteral(
|
||||
$this->decisionQueue[$this->propagateIndex++],
|
||||
$decision[Decisions::DECISION_LITERAL],
|
||||
$level,
|
||||
array($this, 'decisionsContain'),
|
||||
array($this, 'decisionsConflict'),
|
||||
array($this, 'decide')
|
||||
$this->decisions
|
||||
);
|
||||
|
||||
$this->propagateIndex++;
|
||||
|
||||
if ($conflict) {
|
||||
return $conflict;
|
||||
}
|
||||
|
@ -314,30 +228,27 @@ class Solver
|
|||
*/
|
||||
private function revert($level)
|
||||
{
|
||||
while (!empty($this->decisionQueue)) {
|
||||
$literal = $this->decisionQueue[count($this->decisionQueue) - 1];
|
||||
while (!$this->decisions->isEmpty()) {
|
||||
$literal = $this->decisions->lastLiteral();
|
||||
|
||||
if (!$this->decisionMap[abs($literal)]) {
|
||||
if ($this->decisions->undecided($literal)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decisionLevel = abs($this->decisionMap[abs($literal)]);
|
||||
$decisionLevel = $this->decisions->decisionLevel($literal);
|
||||
|
||||
if ($decisionLevel <= $level) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->decisionMap[abs($literal)] = 0;
|
||||
array_pop($this->decisionQueue);
|
||||
array_pop($this->decisionQueueWhy);
|
||||
|
||||
$this->propagateIndex = count($this->decisionQueue);
|
||||
$this->decisions->revertLast();
|
||||
$this->propagateIndex = $this->decisions->getMaxOffset() + 1;
|
||||
}
|
||||
|
||||
while (!empty($this->branches)) {
|
||||
list($literals, $branchLevel) = $this->branches[count($this->branches) - 1];
|
||||
|
||||
if ($branchLevel >= $level) {
|
||||
if ($branchLevel < $level) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -349,7 +260,7 @@ class Solver
|
|||
*
|
||||
* setpropagatelearn
|
||||
*
|
||||
* add free decision (solvable to install) to decisionq
|
||||
* add free decision (a positive literal) to decision queue
|
||||
* increase level and propagate decision
|
||||
* return if no conflict.
|
||||
*
|
||||
|
@ -364,8 +275,7 @@ class Solver
|
|||
{
|
||||
$level++;
|
||||
|
||||
$this->decide($literal, $level, $rule);
|
||||
$this->decisionQueueFree[count($this->decisionQueueWhy) - 1] = true;
|
||||
$this->decisions->decide($literal, $level, $rule, true);
|
||||
|
||||
while (true) {
|
||||
$rule = $this->propagate($level);
|
||||
|
@ -400,10 +310,10 @@ class Solver
|
|||
$this->learnedWhy[$newRule->getId()] = $why;
|
||||
|
||||
$ruleNode = new RuleWatchNode($newRule);
|
||||
$ruleNode->watch2OnHighest($this->decisionMap);
|
||||
$ruleNode->watch2OnHighest($this->decisions);
|
||||
$this->watchGraph->insert($ruleNode);
|
||||
|
||||
$this->decide($learnLiteral, $level, $newRule);
|
||||
$this->decisions->decide($learnLiteral, $level, $newRule);
|
||||
}
|
||||
|
||||
return $level;
|
||||
|
@ -433,7 +343,7 @@ class Solver
|
|||
$seen = array();
|
||||
$learnedLiterals = array(null);
|
||||
|
||||
$decisionId = count($this->decisionQueue);
|
||||
$decisionId = $this->decisions->getMaxOffset() + 1;
|
||||
|
||||
$this->learnedPool[] = array();
|
||||
|
||||
|
@ -442,7 +352,7 @@ class Solver
|
|||
|
||||
foreach ($rule->getLiterals() as $literal) {
|
||||
// skip the one true literal
|
||||
if ($this->decisionsSatisfy($literal)) {
|
||||
if ($this->decisions->satisfy($literal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -451,7 +361,7 @@ class Solver
|
|||
}
|
||||
$seen[abs($literal)] = true;
|
||||
|
||||
$l = abs($this->decisionMap[abs($literal)]);
|
||||
$l = $this->decisions->decisionLevel($literal);
|
||||
|
||||
if (1 === $l) {
|
||||
$l1num++;
|
||||
|
@ -485,7 +395,8 @@ class Solver
|
|||
|
||||
$decisionId--;
|
||||
|
||||
$literal = $this->decisionQueue[$decisionId];
|
||||
$decision = $this->decisions->atOffset($decisionId);
|
||||
$literal = $decision[Decisions::DECISION_LITERAL];
|
||||
|
||||
if (isset($seen[abs($literal)])) {
|
||||
break;
|
||||
|
@ -512,7 +423,8 @@ class Solver
|
|||
}
|
||||
}
|
||||
|
||||
$rule = $this->decisionQueueWhy[$decisionId];
|
||||
$decision = $this->decisions->atOffset($decisionId);
|
||||
$rule = $decision[Decisions::DECISION_REASON];
|
||||
}
|
||||
|
||||
$why = count($this->learnedPool) - 1;
|
||||
|
@ -565,34 +477,35 @@ class Solver
|
|||
|
||||
foreach ($literals as $literal) {
|
||||
// skip the one true literal
|
||||
if ($this->decisionsSatisfy($literal)) {
|
||||
if ($this->decisions->satisfy($literal)) {
|
||||
continue;
|
||||
}
|
||||
$seen[abs($literal)] = true;
|
||||
}
|
||||
|
||||
$decisionId = count($this->decisionQueue);
|
||||
$decisionId = $this->decisions->getMaxOffset() + 1;
|
||||
|
||||
while ($decisionId > 0) {
|
||||
$decisionId--;
|
||||
|
||||
$literal = $this->decisionQueue[$decisionId];
|
||||
$decision = $this->decisions->atOffset($decisionId);
|
||||
$literal = $decision[Decisions::DECISION_LITERAL];
|
||||
|
||||
// skip literals that are not in this rule
|
||||
if (!isset($seen[abs($literal)])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$why = $this->decisionQueueWhy[$decisionId];
|
||||
$problem->addRule($why);
|
||||
$why = $decision[Decisions::DECISION_REASON];
|
||||
|
||||
$problem->addRule($why);
|
||||
$this->analyzeUnsolvableRule($problem, $why);
|
||||
|
||||
$literals = $why->getLiterals();
|
||||
|
||||
foreach ($literals as $literal) {
|
||||
// skip the one true literal
|
||||
if ($this->decisionsSatisfy($literal)) {
|
||||
if ($this->decisions->satisfy($literal)) {
|
||||
continue;
|
||||
}
|
||||
$seen[abs($literal)] = true;
|
||||
|
@ -630,12 +543,8 @@ class Solver
|
|||
|
||||
private function resetSolver()
|
||||
{
|
||||
while ($literal = array_pop($this->decisionQueue)) {
|
||||
$this->decisionMap[abs($literal)] = 0;
|
||||
}
|
||||
$this->decisions->reset();
|
||||
|
||||
$this->decisionQueueWhy = array();
|
||||
$this->decisionQueueFree = array();
|
||||
$this->propagateIndex = 0;
|
||||
$this->branches = array();
|
||||
|
||||
|
@ -717,11 +626,11 @@ class Solver
|
|||
$noneSatisfied = true;
|
||||
|
||||
foreach ($rule->getLiterals() as $literal) {
|
||||
if ($this->decisionsSatisfy($literal)) {
|
||||
if ($this->decisions->satisfy($literal)) {
|
||||
$noneSatisfied = false;
|
||||
break;
|
||||
}
|
||||
if ($literal > 0 && $this->undecided($literal)) {
|
||||
if ($literal > 0 && $this->decisions->undecided($literal)) {
|
||||
$decisionQueue[] = $literal;
|
||||
}
|
||||
}
|
||||
|
@ -794,14 +703,14 @@ class Solver
|
|||
//
|
||||
foreach ($literals as $literal) {
|
||||
if ($literal <= 0) {
|
||||
if (!$this->decidedInstall(abs($literal))) {
|
||||
if (!$this->decisions->decidedInstall(abs($literal))) {
|
||||
continue 2; // next rule
|
||||
}
|
||||
} else {
|
||||
if ($this->decidedInstall(abs($literal))) {
|
||||
if ($this->decisions->decidedInstall(abs($literal))) {
|
||||
continue 2; // next rule
|
||||
}
|
||||
if ($this->undecided(abs($literal))) {
|
||||
if ($this->decisions->undecided(abs($literal))) {
|
||||
$decisionQueue[] = $literal;
|
||||
}
|
||||
}
|
||||
|
@ -834,28 +743,30 @@ class Solver
|
|||
$lastLevel = null;
|
||||
$lastBranchIndex = 0;
|
||||
$lastBranchOffset = 0;
|
||||
$l = 0;
|
||||
|
||||
for ($i = count($this->branches) - 1; $i >= 0; $i--) {
|
||||
list($literals, $level) = $this->branches[$i];
|
||||
list($literals, $l) = $this->branches[$i];
|
||||
|
||||
foreach ($literals as $offset => $literal) {
|
||||
if ($literal && $literal > 0 && $this->decisionMap[abs($literal)] > $level + 1) {
|
||||
if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) {
|
||||
$lastLiteral = $literal;
|
||||
$lastBranchIndex = $i;
|
||||
$lastBranchOffset = $offset;
|
||||
$lastLevel = $level;
|
||||
$lastLevel = $l;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($lastLiteral) {
|
||||
$this->branches[$lastBranchIndex][$lastBranchOffset] = null;
|
||||
unset($this->branches[$lastBranchIndex][0][$lastBranchOffset]);
|
||||
$this->branches[$lastBranchIndex][0] = array_values($this->branches[$lastBranchIndex][0]);
|
||||
$minimizationSteps++;
|
||||
|
||||
$level = $lastLevel;
|
||||
$this->revert($level);
|
||||
|
||||
$why = $this->decisionQueueWhy[count($this->decisionQueueWhy) - 1];
|
||||
$why = $this->decisions->lastReason();
|
||||
|
||||
$oLevel = $level;
|
||||
$level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
|
||||
|
|
|
@ -23,26 +23,164 @@ class Transaction
|
|||
protected $policy;
|
||||
protected $pool;
|
||||
protected $installedMap;
|
||||
protected $decisionMap;
|
||||
protected $decisionQueue;
|
||||
protected $decisionQueueWhy;
|
||||
protected $decisions;
|
||||
protected $transaction;
|
||||
|
||||
public function __construct($policy, $pool, $installedMap, $decisionMap, array $decisionQueue, $decisionQueueWhy)
|
||||
public function __construct($policy, $pool, $installedMap, $decisions)
|
||||
{
|
||||
$this->policy = $policy;
|
||||
$this->pool = $pool;
|
||||
$this->installedMap = $installedMap;
|
||||
$this->decisionMap = $decisionMap;
|
||||
$this->decisionQueue = $decisionQueue;
|
||||
$this->decisionQueueWhy = $decisionQueueWhy;
|
||||
$this->decisions = $decisions;
|
||||
$this->transaction = array();
|
||||
}
|
||||
|
||||
public function getOperations()
|
||||
{
|
||||
$transaction = array();
|
||||
$installMeansUpdateMap = $this->findUpdates();
|
||||
|
||||
$updateMap = array();
|
||||
$installMap = array();
|
||||
$uninstallMap = array();
|
||||
|
||||
foreach ($this->decisions as $i => $decision) {
|
||||
$literal = $decision[Decisions::DECISION_LITERAL];
|
||||
$reason = $decision[Decisions::DECISION_REASON];
|
||||
|
||||
$package = $this->pool->literalToPackage($literal);
|
||||
|
||||
// wanted & installed || !wanted & !installed
|
||||
if (($literal > 0) == (isset($this->installedMap[$package->getId()]))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($literal > 0) {
|
||||
if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) {
|
||||
|
||||
$source = $installMeansUpdateMap[abs($literal)];
|
||||
|
||||
$updateMap[$package->getId()] = array(
|
||||
'package' => $package,
|
||||
'source' => $source,
|
||||
'reason' => $reason,
|
||||
);
|
||||
|
||||
// avoid updates to one package from multiple origins
|
||||
unset($installMeansUpdateMap[abs($literal)]);
|
||||
$ignoreRemove[$source->getId()] = true;
|
||||
} else {
|
||||
$installMap[$package->getId()] = array(
|
||||
'package' => $package,
|
||||
'reason' => $reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->decisions as $i => $decision) {
|
||||
$literal = $decision[Decisions::DECISION_LITERAL];
|
||||
$package = $this->pool->literalToPackage($literal);
|
||||
|
||||
if ($literal <= 0 &&
|
||||
isset($this->installedMap[$package->getId()]) &&
|
||||
!isset($ignoreRemove[$package->getId()])) {
|
||||
$uninstallMap[$package->getId()] = array(
|
||||
'package' => $package,
|
||||
'reason' => $reason,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$this->transactionFromMaps($installMap, $updateMap, $uninstallMap);
|
||||
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
protected function transactionFromMaps($installMap, $updateMap, $uninstallMap)
|
||||
{
|
||||
$queue = array_map(function ($operation) {
|
||||
return $operation['package'];
|
||||
},
|
||||
$this->findRootPackages($installMap, $updateMap)
|
||||
);
|
||||
|
||||
$visited = array();
|
||||
|
||||
while (!empty($queue)) {
|
||||
$package = array_pop($queue);
|
||||
$packageId = $package->getId();
|
||||
|
||||
if (!isset($visited[$packageId])) {
|
||||
array_push($queue, $package);
|
||||
|
||||
if ($package instanceof AliasPackage) {
|
||||
array_push($queue, $package->getAliasOf());
|
||||
} else {
|
||||
foreach ($package->getRequires() as $link) {
|
||||
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
|
||||
|
||||
foreach ($possibleRequires as $require) {
|
||||
array_push($queue, $require);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$visited[$package->getId()] = true;
|
||||
} else {
|
||||
if (isset($installMap[$packageId])) {
|
||||
$this->install(
|
||||
$installMap[$packageId]['package'],
|
||||
$installMap[$packageId]['reason']
|
||||
);
|
||||
unset($installMap[$packageId]);
|
||||
}
|
||||
if (isset($updateMap[$packageId])) {
|
||||
$this->update(
|
||||
$updateMap[$packageId]['source'],
|
||||
$updateMap[$packageId]['package'],
|
||||
$updateMap[$packageId]['reason']
|
||||
);
|
||||
unset($updateMap[$packageId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uninstallMap as $uninstall) {
|
||||
$this->uninstall($uninstall['package'], $uninstall['reason']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function findRootPackages($installMap, $updateMap)
|
||||
{
|
||||
$packages = $installMap + $updateMap;
|
||||
$roots = $packages;
|
||||
|
||||
foreach ($packages as $packageId => $operation) {
|
||||
$package = $operation['package'];
|
||||
|
||||
if (!isset($roots[$packageId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($package->getRequires() as $link) {
|
||||
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
|
||||
|
||||
foreach ($possibleRequires as $require) {
|
||||
unset($roots[$require->getId()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $roots;
|
||||
}
|
||||
|
||||
protected function findUpdates()
|
||||
{
|
||||
$installMeansUpdateMap = array();
|
||||
|
||||
foreach ($this->decisionQueue as $i => $literal) {
|
||||
foreach ($this->decisions as $i => $decision) {
|
||||
$literal = $decision[Decisions::DECISION_LITERAL];
|
||||
$package = $this->pool->literalToPackage($literal);
|
||||
|
||||
// !wanted & installed
|
||||
|
@ -63,105 +201,39 @@ class Transaction
|
|||
}
|
||||
}
|
||||
|
||||
foreach ($this->decisionQueue as $i => $literal) {
|
||||
$package = $this->pool->literalToPackage($literal);
|
||||
return $installMeansUpdateMap;
|
||||
}
|
||||
|
||||
// wanted & installed || !wanted & !installed
|
||||
if (($literal > 0) == (isset($this->installedMap[$package->getId()]))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($literal > 0) {
|
||||
if ($package instanceof AliasPackage) {
|
||||
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||
$package, $this->decisionQueueWhy[$i]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($installMeansUpdateMap[abs($literal)])) {
|
||||
|
||||
$source = $installMeansUpdateMap[abs($literal)];
|
||||
|
||||
$transaction[] = new Operation\UpdateOperation(
|
||||
$source, $package, $this->decisionQueueWhy[$i]
|
||||
);
|
||||
|
||||
// avoid updates to one package from multiple origins
|
||||
unset($installMeansUpdateMap[abs($literal)]);
|
||||
$ignoreRemove[$source->getId()] = true;
|
||||
} else {
|
||||
$transaction[] = new Operation\InstallOperation(
|
||||
$package, $this->decisionQueueWhy[$i]
|
||||
);
|
||||
}
|
||||
} elseif (!isset($ignoreRemove[$package->getId()])) {
|
||||
if ($package instanceof AliasPackage) {
|
||||
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||
$package, $this->decisionQueueWhy[$i]
|
||||
);
|
||||
} else {
|
||||
$transaction[] = new Operation\UninstallOperation(
|
||||
$package, $this->decisionQueueWhy[$i]
|
||||
);
|
||||
}
|
||||
}
|
||||
protected function install($package, $reason)
|
||||
{
|
||||
if ($package instanceof AliasPackage) {
|
||||
return $this->markAliasInstalled($package, $reason);
|
||||
}
|
||||
|
||||
$allDecidedMap = $this->decisionMap;
|
||||
foreach ($this->decisionMap as $packageId => $decision) {
|
||||
if ($decision != 0) {
|
||||
$package = $this->pool->packageById($packageId);
|
||||
if ($package instanceof AliasPackage) {
|
||||
$allDecidedMap[$package->getAliasOf()->getId()] = $decision;
|
||||
}
|
||||
}
|
||||
$this->transaction[] = new Operation\InstallOperation($package, $reason);
|
||||
}
|
||||
|
||||
protected function update($from, $to, $reason)
|
||||
{
|
||||
$this->transaction[] = new Operation\UpdateOperation($from, $to, $reason);
|
||||
}
|
||||
|
||||
protected function uninstall($package, $reason)
|
||||
{
|
||||
if ($package instanceof AliasPackage) {
|
||||
return $this->markAliasUninstalled($package, $reason);
|
||||
}
|
||||
|
||||
foreach ($allDecidedMap as $packageId => $decision) {
|
||||
if ($packageId === 0) {
|
||||
continue;
|
||||
}
|
||||
$this->transaction[] = new Operation\UninstallOperation($package, $reason);
|
||||
}
|
||||
|
||||
if (0 == $decision && isset($this->installedMap[$packageId])) {
|
||||
$package = $this->pool->packageById($packageId);
|
||||
protected function markAliasInstalled($package, $reason)
|
||||
{
|
||||
$this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason);
|
||||
}
|
||||
|
||||
if ($package instanceof AliasPackage) {
|
||||
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||
$package, null
|
||||
);
|
||||
} else {
|
||||
$transaction[] = new Operation\UninstallOperation(
|
||||
$package, null
|
||||
);
|
||||
}
|
||||
|
||||
$this->decisionMap[$packageId] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($allDecidedMap as $packageId => $decision) {
|
||||
if ($packageId === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (0 == $decision && isset($this->installedMap[$packageId])) {
|
||||
$package = $this->pool->packageById($packageId);
|
||||
|
||||
if ($package instanceof AliasPackage) {
|
||||
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||
$package, null
|
||||
);
|
||||
} else {
|
||||
$transaction[] = new Operation\UninstallOperation(
|
||||
$package, null
|
||||
);
|
||||
}
|
||||
|
||||
$this->decisionMap[$packageId] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return array_reverse($transaction);
|
||||
protected function markAliasUninstalled($package, $reason)
|
||||
{
|
||||
$this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,33 @@ class SolverTest extends TestCase
|
|||
));
|
||||
}
|
||||
|
||||
public function testSolverInstallWithDepsInOrder()
|
||||
{
|
||||
$this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
|
||||
$this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
|
||||
$this->repo->addPackage($packageC = $this->getPackage('C', '1.0'));
|
||||
|
||||
$packageB->setRequires(array(
|
||||
new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
|
||||
new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
|
||||
));
|
||||
$packageC->setRequires(array(
|
||||
new Link('C', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
|
||||
));
|
||||
|
||||
$this->reposComplete();
|
||||
|
||||
$this->request->install('A');
|
||||
$this->request->install('B');
|
||||
$this->request->install('C');
|
||||
|
||||
$this->checkSolverResult(array(
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
array('job' => 'install', 'package' => $packageC),
|
||||
array('job' => 'install', 'package' => $packageB),
|
||||
));
|
||||
}
|
||||
|
||||
public function testSolverInstallInstalled()
|
||||
{
|
||||
$this->repoInstalled->addPackage($this->getPackage('A', '1.0'));
|
||||
|
@ -291,15 +318,16 @@ class SolverTest extends TestCase
|
|||
$this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0'));
|
||||
|
||||
$this->checkSolverResult(array(
|
||||
array(
|
||||
'job' => 'remove',
|
||||
'package' => $packageB,
|
||||
),
|
||||
array(
|
||||
'job' => 'update',
|
||||
'from' => $packageA,
|
||||
'to' => $newPackageA,
|
||||
)));
|
||||
),
|
||||
array(
|
||||
'job' => 'remove',
|
||||
'package' => $packageB,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function testSolverAllJobs()
|
||||
|
@ -324,8 +352,8 @@ class SolverTest extends TestCase
|
|||
$this->checkSolverResult(array(
|
||||
array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC),
|
||||
array('job' => 'install', 'package' => $packageB),
|
||||
array('job' => 'remove', 'package' => $packageD),
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
array('job' => 'remove', 'package' => $packageD),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -477,8 +505,8 @@ class SolverTest extends TestCase
|
|||
$this->request->install('X');
|
||||
|
||||
$this->checkSolverResult(array(
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
array('job' => 'install', 'package' => $newPackageB),
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
array('job' => 'install', 'package' => $packageX),
|
||||
));
|
||||
}
|
||||
|
@ -520,9 +548,9 @@ class SolverTest extends TestCase
|
|||
$this->request->install('A');
|
||||
|
||||
$this->checkSolverResult(array(
|
||||
array('job' => 'install', 'package' => $packageC),
|
||||
array('job' => 'install', 'package' => $packageB),
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
array('job' => 'install', 'package' => $packageC),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -690,8 +718,8 @@ class SolverTest extends TestCase
|
|||
$this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0'));
|
||||
|
||||
$this->checkSolverResult(array(
|
||||
array('job' => 'install', 'package' => $packageB),
|
||||
array('job' => 'install', 'package' => $packageA2),
|
||||
array('job' => 'install', 'package' => $packageB),
|
||||
array('job' => 'install', 'package' => $packageA2Alias),
|
||||
));
|
||||
}
|
||||
|
@ -713,9 +741,9 @@ class SolverTest extends TestCase
|
|||
$this->request->install('B');
|
||||
|
||||
$this->checkSolverResult(array(
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
array('job' => 'install', 'package' => $packageAAlias),
|
||||
array('job' => 'install', 'package' => $packageB),
|
||||
array('job' => 'install', 'package' => $packageA),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ Aliases take precedence over default package even if default is selected
|
|||
--RUN--
|
||||
install
|
||||
--EXPECT--
|
||||
Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f)
|
||||
Installing a/req (dev-feature-foo feat.f)
|
||||
Installing a/b (dev-master)
|
||||
Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f)
|
||||
Installing a/a (dev-master)
|
||||
Installing a/b (dev-master)
|
||||
|
|
|
@ -47,9 +47,9 @@ Aliases take precedence over default package
|
|||
--RUN--
|
||||
install
|
||||
--EXPECT--
|
||||
Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)
|
||||
Installing a/b (dev-master forked)
|
||||
Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f)
|
||||
Installing a/a (dev-master master)
|
||||
Installing a/c (dev-feature-foo feat.f)
|
||||
Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f)
|
||||
Installing a/b (dev-master forked)
|
||||
Installing a/a (dev-master master)
|
||||
Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master)
|
||||
Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)
|
||||
|
|
Loading…
Reference in New Issue