mirror of https://github.com/sbt/sbt.git
new aggregation approach, still need exclusion mechanism
This commit is contained in:
parent
ec48779829
commit
82326cc899
112
main/Act.scala
112
main/Act.scala
|
|
@ -7,11 +7,13 @@ package sbt
|
|||
import Keys.{sessionSettings, thisProject}
|
||||
import Load.BuildStructure
|
||||
import complete.{DefaultParsers, Parser}
|
||||
import Aggregation.{KeyValue,Values}
|
||||
import DefaultParsers._
|
||||
import Types.idFun
|
||||
import java.net.URI
|
||||
import CommandSupport.ShowCommand
|
||||
|
||||
final class ParsedKey(val key: ScopedKey[_], val mask: ScopeMask)
|
||||
object Act
|
||||
{
|
||||
val GlobalString = "*"
|
||||
|
|
@ -19,29 +21,46 @@ object Act
|
|||
// this does not take aggregation into account
|
||||
def scopedKey(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String],
|
||||
keyMap: Map[String, AttributeKey[_]], data: Settings[Scope]): Parser[ScopedKey[_]] =
|
||||
scopedKeySelected(index, current, defaultConfigs, keyMap, data).map(_.key)
|
||||
|
||||
// the index should be an aggregated index for proper tab completion
|
||||
def scopedKeyAggregated(current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], structure: BuildStructure): KeysParser =
|
||||
for(selected <- scopedKeySelected(structure.index.aggregateKeyIndex, current, defaultConfigs, structure.index.keyMap, structure.data) ) yield
|
||||
Resolve.aggregateDeps(selected.key, selected.mask, structure.extra)
|
||||
|
||||
def scopedKeySelected(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String],
|
||||
keyMap: Map[String, AttributeKey[_]], data: Settings[Scope]): Parser[ParsedKey] =
|
||||
scopedKeyFull(index, current, defaultConfigs, keyMap) flatMap { choices =>
|
||||
select(choices, data)( Project.showRelativeKey(current, index.buildURIs.size > 1) )
|
||||
}
|
||||
|
||||
def scopedKeyFull(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], keyMap: Map[String, AttributeKey[_]]): Parser[Seq[Parser[ParsedKey]]] =
|
||||
{
|
||||
implicit val show = Project.showRelativeKey(current, index.buildURIs.size > 1)
|
||||
def taskKeyExtra(proj: Option[ResolvedReference], confAmb: ParsedAxis[String]): Seq[Parser[ScopedKey[_]]] =
|
||||
def taskKeyExtra(proj: Option[ResolvedReference], confAmb: ParsedAxis[String], baseMask: ScopeMask): Seq[Parser[ParsedKey]] =
|
||||
for {
|
||||
conf <- configs(confAmb, defaultConfigs, proj, index)
|
||||
} yield for {
|
||||
task <- taskAxis(conf, index.tasks(proj, conf), keyMap) map resolveTask
|
||||
key <- key(index, proj, conf, task, keyMap, data)
|
||||
taskAmb <- taskAxis(conf, index.tasks(proj, conf), keyMap)
|
||||
task = resolveTask(taskAmb)
|
||||
key <- key(index, proj, conf, task, keyMap)
|
||||
extra <- extraAxis(keyMap, IMap.empty)
|
||||
} yield
|
||||
makeScopedKey( proj, conf, task, extra, key )
|
||||
} yield {
|
||||
val mask = baseMask.copy(task = taskAmb.isExplicit, extra = true)
|
||||
new ParsedKey( makeScopedKey( proj, conf, task, extra, key ), mask)
|
||||
}
|
||||
|
||||
for {
|
||||
proj <- optProjectRef(index, current)
|
||||
rawProject <- optProjectRef(index, current)
|
||||
proj = resolveProject(rawProject, current)
|
||||
confAmb <- config( index configs proj )
|
||||
result <- select(taskKeyExtra(proj, confAmb), data)
|
||||
partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false)
|
||||
} yield
|
||||
result
|
||||
taskKeyExtra(proj, confAmb, partialMask)
|
||||
}
|
||||
def makeScopedKey(proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]], extra: ScopeAxis[AttributeMap], key: AttributeKey[_]): ScopedKey[_] =
|
||||
ScopedKey( Scope( toAxis(proj, Global), toAxis(conf map ConfigKey.apply, Global), toAxis(task, Global), extra), key )
|
||||
|
||||
def select(allKeys: Seq[Parser[ScopedKey[_]]], data: Settings[Scope])(implicit show: Show[ScopedKey[_]]): Parser[ScopedKey[_]] =
|
||||
def select(allKeys: Seq[Parser[ParsedKey]], data: Settings[Scope])(implicit show: Show[ScopedKey[_]]): Parser[ParsedKey] =
|
||||
seq(allKeys) flatMap { ss =>
|
||||
val default = ss.headOption match {
|
||||
case None => noValidKeys
|
||||
|
|
@ -49,27 +68,28 @@ object Act
|
|||
}
|
||||
selectFromValid(ss filter isValid(data), default)
|
||||
}
|
||||
def selectFromValid(ss: Seq[ScopedKey[_]], default: Parser[ScopedKey[_]])(implicit show: Show[ScopedKey[_]]): Parser[ScopedKey[_]] =
|
||||
def selectFromValid(ss: Seq[ParsedKey], default: Parser[ParsedKey])(implicit show: Show[ScopedKey[_]]): Parser[ParsedKey] =
|
||||
selectByTask(selectByConfig(ss)) match
|
||||
{
|
||||
case Seq() => default
|
||||
case Seq(single) => success(single)
|
||||
case multi => failure("Ambiguous keys: " + showAmbiguous(multi))
|
||||
case multi => failure("Ambiguous keys: " + showAmbiguous(keys(multi)))
|
||||
}
|
||||
def selectByConfig(ss: Seq[ScopedKey[_]]): Seq[ScopedKey[_]] =
|
||||
private[this] def keys(ss: Seq[ParsedKey]): Seq[ScopedKey[_]] = ss.map(_.key)
|
||||
def selectByConfig(ss: Seq[ParsedKey]): Seq[ParsedKey] =
|
||||
ss match
|
||||
{
|
||||
case Seq() => Nil
|
||||
case Seq(x, tail @ _*) => // select the first configuration containing a valid key
|
||||
tail.takeWhile(_.scope.config == x.scope.config) match
|
||||
tail.takeWhile(_.key.scope.config == x.key.scope.config) match
|
||||
{
|
||||
case Seq() => x :: Nil
|
||||
case xs => x +: xs
|
||||
}
|
||||
}
|
||||
def selectByTask(ss: Seq[ScopedKey[_]]): Seq[ScopedKey[_]] =
|
||||
def selectByTask(ss: Seq[ParsedKey]): Seq[ParsedKey] =
|
||||
{
|
||||
val (selects, globals) = ss.partition(_.scope.task.isSelect)
|
||||
val (selects, globals) = ss.partition(_.key.scope.task.isSelect)
|
||||
if(globals.nonEmpty) globals else selects
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +98,11 @@ object Act
|
|||
def showAmbiguous(keys: Seq[ScopedKey[_]])(implicit show: Show[ScopedKey[_]]): String =
|
||||
keys.take(3).map(x => show(x)).mkString("", ", ", if(keys.size > 3) ", ..." else "")
|
||||
|
||||
def isValid(data: Settings[Scope])(key: ScopedKey[_]): Boolean =
|
||||
def isValid(data: Settings[Scope])(parsed: ParsedKey): Boolean =
|
||||
{
|
||||
val key = parsed.key
|
||||
data.definingScope(key.scope, key.key) == Some(key.scope)
|
||||
}
|
||||
|
||||
def examples(p: Parser[String], exs: Set[String], label: String): Parser[String] =
|
||||
p !!! ("Expected " + label) examples exs
|
||||
|
|
@ -90,8 +113,6 @@ object Act
|
|||
p.? map { opt => toAxis(opt, ifNone) }
|
||||
def toAxis[T](opt: Option[T], ifNone: ScopeAxis[T]): ScopeAxis[T] =
|
||||
opt match { case Some(t) => Select(t); case None => ifNone }
|
||||
def defaultConfigs(data: Settings[Scope])(project: ProjectRef): Seq[String] =
|
||||
thisProject in project get data map( _.configurations.map(_.name)) getOrElse Nil
|
||||
|
||||
def config(confs: Set[String]): Parser[ParsedAxis[String]] =
|
||||
token( (GlobalString ^^^ ParsedGlobal | value(examples(ID, confs, "configuration")) ) <~ ':' ) ?? Omitted
|
||||
|
|
@ -106,8 +127,7 @@ object Act
|
|||
def nonEmptyConfig(index: KeyIndex, proj: Option[ResolvedReference]): String => Seq[Option[String]] = config =>
|
||||
if(index.isEmpty(proj, Some(config))) Nil else Some(config) :: Nil
|
||||
|
||||
def key(index: KeyIndex, proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]], keyMap: Map[String,AttributeKey[_]], data: Settings[Scope]):
|
||||
Parser[AttributeKey[_]] =
|
||||
def key(index: KeyIndex, proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]], keyMap: Map[String,AttributeKey[_]]): Parser[AttributeKey[_]] =
|
||||
{
|
||||
def keyParser(keys: Set[String]): Parser[AttributeKey[_]] =
|
||||
token(ID !!! "Expected key" examples keys) flatMap { keyString=>
|
||||
|
|
@ -169,10 +189,10 @@ object Act
|
|||
def knownIDParser[T](knownKeys: Map[String, T], label: String): Parser[T] =
|
||||
token(examplesStrict(ID, knownKeys.keys.toSet, label)) map knownKeys
|
||||
|
||||
def projectRef(index: KeyIndex, currentBuild: URI): Parser[Option[ResolvedReference]] =
|
||||
def projectRef(index: KeyIndex, currentBuild: URI): Parser[ParsedAxis[ResolvedReference]] =
|
||||
{
|
||||
val global = token(GlobalString ~ '/') ^^^ None
|
||||
global | some(resolvedReference(index, currentBuild, '/'))
|
||||
val global = token(GlobalString ~ '/') ^^^ ParsedGlobal
|
||||
global | value(resolvedReference(index, currentBuild, '/'))
|
||||
}
|
||||
def resolvedReference(index: KeyIndex, currentBuild: URI, trailing: Parser[_]): Parser[ResolvedReference] =
|
||||
{
|
||||
|
|
@ -188,37 +208,55 @@ object Act
|
|||
case Some(uri) => projectRef(uri) | token(trailing ^^^ BuildRef(uri))
|
||||
}
|
||||
}
|
||||
def optProjectRef(index: KeyIndex, current: ProjectRef): Parser[Option[ResolvedReference]] =
|
||||
projectRef(index, current.build) ?? Some(current)
|
||||
def optProjectRef(index: KeyIndex, current: ProjectRef): Parser[ParsedAxis[ResolvedReference]] =
|
||||
projectRef(index, current.build) ?? Omitted
|
||||
def resolveProject(parsed: ParsedAxis[ResolvedReference], current: ProjectRef): Option[ResolvedReference] =
|
||||
parsed match
|
||||
{
|
||||
case Omitted => Some(current)
|
||||
case ParsedGlobal => None
|
||||
case pv: ParsedValue[ResolvedReference] => Some(pv.value)
|
||||
}
|
||||
|
||||
def actParser(s: State): Parser[() => State] = requireSession(s, actParser0(s))
|
||||
|
||||
private[this] def actParser0(state: State) =
|
||||
private[this] def actParser0(state: State): Parser[() => State] =
|
||||
{
|
||||
val extracted = Project extract state
|
||||
import extracted.{showKey, structure}
|
||||
showParser.flatMap { show =>
|
||||
scopedKeyParser(extracted) flatMap Aggregation.valueParser(state, structure, show)
|
||||
aggregatedKeyParser(extracted) flatMap { kvs =>
|
||||
Aggregation.evaluatingParser(state, structure, show)( keyValues(structure)(kvs) )
|
||||
}
|
||||
}
|
||||
}
|
||||
def showParser = token( (ShowCommand ~ Space) ^^^ true) ?? false
|
||||
def scopedKeyParser(state: State): Parser[ScopedKey[_]] = scopedKeyParser(Project extract state)
|
||||
def scopedKeyParser(extracted: Extracted): Parser[ScopedKey[_]] = scopedKeyParser(extracted.structure, extracted.currentRef)
|
||||
def scopedKeyParser(structure: BuildStructure, currentRef: ProjectRef): Parser[ScopedKey[_]] =
|
||||
{
|
||||
def confs(uri: URI) = if(structure.units.contains(uri)) defaultConfigs(structure.data)(ProjectRef(uri, structure.rootProject(uri))) else Nil
|
||||
val defaultConfs: Option[ResolvedReference] => Seq[String] = {
|
||||
case None => confs(structure.root)
|
||||
case Some(BuildRef(uri)) => confs(uri)
|
||||
case Some(ref: ProjectRef) => if(Project.getProject(ref, structure).isDefined) defaultConfigs(structure.data)(ref) else Nil
|
||||
scopedKey(structure.index.keyIndex, currentRef, structure.extra.configurationsForAxis, structure.index.keyMap, structure.data)
|
||||
|
||||
type KeysParser = Parser[Seq[ScopedKey[T]] forSome { type T}]
|
||||
def aggregatedKeyParser(state: State): KeysParser = aggregatedKeyParser(Project extract state)
|
||||
def aggregatedKeyParser(extracted: Extracted): KeysParser = aggregatedKeyParser(extracted.structure, extracted.currentRef)
|
||||
def aggregatedKeyParser(structure: BuildStructure, currentRef: ProjectRef): KeysParser =
|
||||
scopedKeyAggregated(currentRef, structure.extra.configurationsForAxis, structure)
|
||||
|
||||
def keyValues[T](state: State)(keys: Seq[ScopedKey[T]]): Values[T] = keyValues(Project extract state)(keys)
|
||||
def keyValues[T](extracted: Extracted)(keys: Seq[ScopedKey[T]]): Values[T] = keyValues(extracted.structure)(keys)
|
||||
def keyValues[T](structure: BuildStructure)(keys: Seq[ScopedKey[T]]): Values[T] =
|
||||
keys.flatMap { key =>
|
||||
structure.data.get(key.scope, key.key) map { value =>
|
||||
KeyValue(key, value)
|
||||
}
|
||||
}
|
||||
scopedKey(structure.index.keyIndex, currentRef, defaultConfs, structure.index.keyMap, structure.data)
|
||||
}
|
||||
|
||||
def requireSession[T](s: State, p: => Parser[T]): Parser[T] =
|
||||
if(s get sessionSettings isEmpty) failure("No project loaded") else p
|
||||
|
||||
sealed trait ParsedAxis[+T]
|
||||
sealed trait ParsedAxis[+T] {
|
||||
final def isExplicit = this != Omitted
|
||||
}
|
||||
final object ParsedGlobal extends ParsedAxis[Nothing]
|
||||
final object Omitted extends ParsedAxis[Nothing]
|
||||
final class ParsedValue[T](val value: T) extends ParsedAxis[T]
|
||||
|
|
|
|||
|
|
@ -22,42 +22,6 @@ final object Aggregation
|
|||
final class Explicit(val dependencies: Seq[ProjectReference], val transitive: Boolean) extends Aggregation
|
||||
|
||||
final case class KeyValue[+T](key: ScopedKey[_], value: T)
|
||||
def getTasks[T](key: ScopedKey[T], structure: BuildStructure, transitive: Boolean): Seq[KeyValue[T]] =
|
||||
getTasks0(key, structure, transitive, new mutable.HashMap[(ScopedKey[_], Boolean), Seq[KeyValue[T]]])
|
||||
private type Memo[T] = mutable.Map[(ScopedKey[_], Boolean), Seq[KeyValue[T]]]
|
||||
private[this] def getTasks0[T](key: ScopedKey[T], structure: BuildStructure, transitive: Boolean, memo: Memo[T]): Seq[KeyValue[T]] =
|
||||
memo.getOrElseUpdate( (key, transitive), {
|
||||
val task = structure.data.get(key.scope, key.key).toList.map(t => KeyValue(key,t))
|
||||
if(transitive) aggregateDeps(key, structure, memo) ++ task else task
|
||||
})
|
||||
def projectAggregate(key: ScopedKey[_], structure: BuildStructure): Seq[ProjectRef] =
|
||||
{
|
||||
val project = key.scope.project.toOption.flatMap { ref => Project.getProjectForReference(ref, structure) }
|
||||
project match { case Some(p) => p.aggregate; case None => Nil }
|
||||
}
|
||||
private[this] def aggregateDeps[T](key: ScopedKey[T], structure: BuildStructure, memo: Memo[T]): Seq[KeyValue[T]] =
|
||||
{
|
||||
val aggregated = aggregate in Scope.fillTaskAxis(key.scope, key.key) get structure.data getOrElse Enabled
|
||||
val (agg, transitive) =
|
||||
aggregated match
|
||||
{
|
||||
case Implicit(false) => (Nil, false)
|
||||
case Implicit(true) => (projectAggregate(key, structure), true)
|
||||
case e: Explicit => (e.dependencies, e.transitive)
|
||||
}
|
||||
val currentBuild = key.scope.project.toOption.flatMap { case ProjectRef(uri, _) => Some(uri); case BuildRef(ref) => Some(ref); case _ => None }
|
||||
agg flatMap { a =>
|
||||
val resolved = subCurrentBuild(a, currentBuild)
|
||||
val newKey = ScopedKey(key.scope.copy(project = Select(resolved)), key.key)
|
||||
getTasks(newKey, structure, transitive)
|
||||
}
|
||||
}
|
||||
private def subCurrentBuild(ref: Reference, currentBuild: Option[URI]): Reference =
|
||||
currentBuild match
|
||||
{
|
||||
case None => ref
|
||||
case Some(current) => Scope.resolveBuildOnly(current, ref)
|
||||
}
|
||||
|
||||
def printSettings[T](xs: Seq[KeyValue[T]], log: Logger)(implicit display: Show[ScopedKey[_]]) =
|
||||
xs match
|
||||
|
|
@ -66,6 +30,7 @@ final object Aggregation
|
|||
case _ => xs foreach { case KeyValue(key, value) => log.info(display(key) + "\n\t" + value.toString) }
|
||||
}
|
||||
type Values[T] = Seq[KeyValue[T]]
|
||||
type AnyKeys = Values[_]
|
||||
def seqParser[T](ps: Values[Parser[T]]): Parser[Seq[KeyValue[T]]] = seq(ps.map { case KeyValue(k,p) => p.map(v => KeyValue(k,v) ) })
|
||||
|
||||
def applyTasks[T](s: State, structure: BuildStructure, ps: Values[Parser[Task[T]]], show: Boolean)(implicit display: Show[ScopedKey[_]]): Parser[() => State] =
|
||||
|
|
@ -141,10 +106,11 @@ final object Aggregation
|
|||
runTasks(s, structure, roots, dummies, show)
|
||||
}
|
||||
}
|
||||
def valueParser(s: State, structure: BuildStructure, show: Boolean)(key: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): Parser[() => State] =
|
||||
getTasks(key, structure, true).toList match
|
||||
|
||||
def evaluatingParser[T](s: State, structure: BuildStructure, show: Boolean)(keys: Seq[KeyValue[T]])(implicit display: Show[ScopedKey[_]]): Parser[() => State] =
|
||||
keys.toList match
|
||||
{
|
||||
case Nil => failure("No such setting/task: " + display(key))
|
||||
case Nil => failure("No such setting/task")
|
||||
case xs @ KeyValue(_, _: InputStatic[t]) :: _ => applyTasks(s, structure, maps(xs.asInstanceOf[Values[InputStatic[t]]])(_.parser(s)), show)
|
||||
case xs @ KeyValue(_, _: InputDynamic[t]) :: _ => applyDynamicTasks(s, structure, xs.asInstanceOf[Values[InputDynamic[t]]], show)
|
||||
case xs @ KeyValue(_, _: Task[t]) :: _ => applyTasks(s, structure, maps(xs.asInstanceOf[Values[Task[t]]])(x => success(x)), show)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package sbt
|
||||
|
||||
import java.net.URI
|
||||
|
||||
final class BuildUtil[Proj](
|
||||
val keyIndex: KeyIndex,
|
||||
val root: URI,
|
||||
val rootProjectID: URI => String,
|
||||
val project: (URI, String) => Proj,
|
||||
val configurations: Proj => Seq[ConfigKey],
|
||||
val aggregates: Relation[ProjectRef, ProjectRef]
|
||||
)
|
||||
{
|
||||
def rootProject(uri: URI): Proj =
|
||||
project(uri, rootProjectID(uri))
|
||||
|
||||
def resolveRef(ref: Reference): ResolvedReference =
|
||||
Scope.resolveReference(root, rootProjectID, ref)
|
||||
|
||||
def projectFor(ref: ResolvedReference): Proj = ref match {
|
||||
case ProjectRef(uri, id) => project(uri, id)
|
||||
case BuildRef(uri) => rootProject(uri)
|
||||
}
|
||||
def projectRefFor(ref: ResolvedReference): ProjectRef = ref match {
|
||||
case p: ProjectRef => p
|
||||
case BuildRef(uri) => ProjectRef(uri, rootProjectID(uri))
|
||||
}
|
||||
def projectForAxis(ref: Option[ResolvedReference]): Proj = ref match {
|
||||
case Some(ref) => projectFor(ref)
|
||||
case None => rootProject(root)
|
||||
}
|
||||
def exactProject(refOpt: Option[Reference]): Option[Proj] = refOpt map resolveRef flatMap {
|
||||
case ProjectRef(uri, id) => Some(project(uri, id))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
val configurationsForAxis: Option[ResolvedReference] => Seq[String] =
|
||||
refOpt => configurations(projectForAxis(refOpt)).map(_.name)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ package sbt
|
|||
|
||||
import java.net.URI
|
||||
import Project.ScopedKey
|
||||
import Load.BuildStructure
|
||||
import complete.DefaultParsers.validID
|
||||
import Types.{idFun, some}
|
||||
|
||||
|
|
@ -13,6 +14,9 @@ object KeyIndex
|
|||
def empty: ExtendableKeyIndex = new KeyIndex0(emptyBuildIndex)
|
||||
def apply(known: Iterable[ScopedKey[_]]): ExtendableKeyIndex =
|
||||
(empty /: known) { _ add _ }
|
||||
def aggregate(known: Iterable[ScopedKey[_]], extra: BuildUtil[_]): ExtendableKeyIndex =
|
||||
(empty /: known) { (index, key) => index.addAggregated(key, extra) }
|
||||
|
||||
def combine(indices: Seq[KeyIndex]): KeyIndex = new KeyIndex {
|
||||
def buildURIs = concat(_.buildURIs)
|
||||
def projects(uri: URI) = concat(_.projects(uri))
|
||||
|
|
@ -52,6 +56,7 @@ trait KeyIndex
|
|||
trait ExtendableKeyIndex extends KeyIndex
|
||||
{
|
||||
def add(scoped: ScopedKey[_]): ExtendableKeyIndex
|
||||
def addAggregated(scoped: ScopedKey[_], extra: BuildUtil[_]): ExtendableKeyIndex
|
||||
}
|
||||
// task axis <-> key
|
||||
private final class AKeyIndex(val data: Relation[ Option[AttributeKey[_]], String])
|
||||
|
|
@ -110,13 +115,22 @@ private final class KeyIndex0(val data: BuildIndex) extends ExtendableKeyIndex
|
|||
}
|
||||
private[this] def optConfigs(project: Option[ResolvedReference]): Seq[Option[String]] = None +: (configs(project).toSeq map some.fn)
|
||||
|
||||
def addAggregated(scoped: ScopedKey[_], extra: BuildUtil[_]): ExtendableKeyIndex =
|
||||
if(validID(scoped.key.label))
|
||||
{
|
||||
val aggregateProjects = Resolve.aggregateDeps(scoped, ScopeMask(), extra, reverse = true)
|
||||
((this: ExtendableKeyIndex) /: aggregateProjects)(_ add _)
|
||||
}
|
||||
else
|
||||
this
|
||||
|
||||
def add(scoped: ScopedKey[_]): ExtendableKeyIndex =
|
||||
if(validID(scoped.key.label)) add0(scoped) else this
|
||||
private[this] def add0(scoped: ScopedKey[_]): ExtendableKeyIndex =
|
||||
{
|
||||
val (build, project) = parts(scoped.scope.project.toOption)
|
||||
add(build, project, scoped.scope.config, scoped.scope.task, scoped.key)
|
||||
add1(build, project, scoped.scope.config, scoped.scope.task, scoped.key)
|
||||
}
|
||||
def add(uri: Option[URI], id: Option[String], config: ScopeAxis[ConfigKey], task: ScopeAxis[AttributeKey[_]], key: AttributeKey[_]): ExtendableKeyIndex =
|
||||
private[this] def add1(uri: Option[URI], id: Option[String], config: ScopeAxis[ConfigKey], task: ScopeAxis[AttributeKey[_]], key: AttributeKey[_]): ExtendableKeyIndex =
|
||||
new KeyIndex0( data.add(uri, id, config.toOption.map(_.name), task.toOption, key) )
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ object Load
|
|||
val settings = finalTransforms(buildConfigurations(loaded, getRootProject(projects), rootEval, config.injectSettings))
|
||||
val delegates = config.delegates(loaded)
|
||||
val data = Project.makeSettings(settings, delegates, config.scopeLocal)( Project.showLoadingKey( loaded ) )
|
||||
val index = structureIndex(data, settings)
|
||||
val index = structureIndex(data, settings, loaded.extra)
|
||||
val streams = mkStreams(projects, loaded.root, data)
|
||||
(rootEval, new BuildStructure(projects, loaded.root, settings, data, index, streams, delegates, config.scopeLocal))
|
||||
}
|
||||
|
|
@ -160,12 +160,14 @@ object Load
|
|||
def setDefinitionKey[T](tk: Task[T], key: ScopedKey[_]): Task[T] =
|
||||
if(isDummy(tk)) tk else Task(tk.info.set(Keys.taskDefinitionKey, key), tk.work)
|
||||
|
||||
def structureIndex(data: Settings[Scope], settings: Seq[Setting[_]]): StructureIndex =
|
||||
def structureIndex(data: Settings[Scope], settings: Seq[Setting[_]], extra: KeyIndex => BuildUtil[_]): StructureIndex =
|
||||
{
|
||||
val keys = Index.allKeys(settings)
|
||||
val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key)
|
||||
val scopedKeys = keys ++ data.allKeys( (s,k) => ScopedKey(s,k))
|
||||
new StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), KeyIndex(scopedKeys))
|
||||
val keyIndex = KeyIndex(scopedKeys)
|
||||
val aggIndex = KeyIndex.aggregate(scopedKeys, extra(keyIndex))
|
||||
new StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex)
|
||||
}
|
||||
|
||||
// Reevaluates settings after modifying them. Does not recompile or reload any build components.
|
||||
|
|
@ -173,7 +175,7 @@ object Load
|
|||
{
|
||||
val transformed = finalTransforms(newSettings)
|
||||
val newData = Project.makeSettings(transformed, structure.delegates, structure.scopeLocal)
|
||||
val newIndex = structureIndex(newData, transformed)
|
||||
val newIndex = structureIndex(newData, transformed, index => buildUtil(structure.root, structure.units, index))
|
||||
val newStreams = mkStreams(structure.units, structure.root, newData)
|
||||
new BuildStructure(units = structure.units, root = structure.root, settings = transformed, data = newData, index = newIndex, streams = newStreams, delegates = structure.delegates, scopeLocal = structure.scopeLocal)
|
||||
}
|
||||
|
|
@ -545,6 +547,7 @@ object Load
|
|||
{
|
||||
checkCycles(units)
|
||||
def allProjectRefs: Seq[(ProjectRef, ResolvedProject)] = for( (uri, unit) <- units.toSeq; (id, proj) <- unit.defined ) yield ProjectRef(uri, id) -> proj
|
||||
def extra(keyIndex: KeyIndex): BuildUtil[ResolvedProject] = buildUtil(root, units, keyIndex)
|
||||
}
|
||||
def checkCycles(units: Map[URI, LoadedBuildUnit])
|
||||
{
|
||||
|
|
@ -567,6 +570,7 @@ object Load
|
|||
final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, ResolvedProject], val rootProjects: Seq[String], val buildSettings: Seq[Setting[_]]) extends BuildUnitBase
|
||||
{
|
||||
assert(!rootProjects.isEmpty, "No root projects defined for build unit " + unit)
|
||||
val root = rootProjects.head
|
||||
def localBase = unit.localBase
|
||||
def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath
|
||||
def loader = unit.definitions.loader
|
||||
|
|
@ -584,8 +588,16 @@ object Load
|
|||
def allProjects(build: URI): Seq[ResolvedProject] = units(build).defined.values.toSeq
|
||||
def allProjectRefs: Seq[ProjectRef] = units.toSeq flatMap { case (build, unit) => refs(build, unit.defined.values.toSeq) }
|
||||
def allProjectRefs(build: URI): Seq[ProjectRef] = refs(build, allProjects(build))
|
||||
val extra: BuildUtil[ResolvedProject] = buildUtil(root, units, index.keyIndex)
|
||||
private[this] def refs(build: URI, projects: Seq[ResolvedProject]): Seq[ProjectRef] = projects.map { p => ProjectRef(build, p.id) }
|
||||
}
|
||||
def buildUtil(root: URI, units: Map[URI, LoadedBuildUnit], keyIndex: KeyIndex): BuildUtil[ResolvedProject] =
|
||||
{
|
||||
val getp = (build: URI, project: String) => Load.getProject(units, build, project)
|
||||
val configs = (_: ResolvedProject).configurations.map(c => ConfigKey(c.name))
|
||||
val aggregates = Resolve.aggregates(units)
|
||||
new BuildUtil(keyIndex, root, Load getRootProject units, getp, configs, aggregates)
|
||||
}
|
||||
final case class LoadBuildConfiguration(stagingDirectory: File, classpath: Seq[Attributed[File]], loader: ClassLoader, compilers: Compilers, evalPluginDef: (BuildStructure, State) => Seq[Attributed[File]], definesClass: DefinesClass, delegates: LoadedBuild => Scope => Seq[Scope], scopeLocal: ScopeLocal, injectSettings: InjectSettings, globalPlugin: Option[GlobalPlugin], log: Logger)
|
||||
{
|
||||
lazy val (globalPluginClasspath, globalPluginLoader) = pluginDefinitionLoader(this, Load.globalPluginClasspath(globalPlugin))
|
||||
|
|
@ -594,5 +606,11 @@ object Load
|
|||
final case class InjectSettings(global: Seq[Setting[_]], project: Seq[Setting[_]], projectLoaded: ClassLoader => Seq[Setting[_]])
|
||||
|
||||
// information that is not original, but can be reconstructed from the rest of BuildStructure
|
||||
final class StructureIndex(val keyMap: Map[String, AttributeKey[_]], val taskToKey: Map[Task[_], ScopedKey[Task[_]]], val triggers: Triggers[Task], val keyIndex: KeyIndex)
|
||||
final class StructureIndex(
|
||||
val keyMap: Map[String, AttributeKey[_]],
|
||||
val taskToKey: Map[Task[_], ScopedKey[Task[_]]],
|
||||
val triggers: Triggers[Task],
|
||||
val keyIndex: KeyIndex,
|
||||
val aggregateKeyIndex: KeyIndex
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ package sbt
|
|||
import HistoryCommands.{Start => HistoryPrefix}
|
||||
import compiler.EvalImports
|
||||
import Types.{const,idFun}
|
||||
import Aggregation.AnyKeys
|
||||
|
||||
import Command.applyEffect
|
||||
import Keys.{analysis,historyPath,globalLogging,shellPrompt}
|
||||
|
|
@ -427,9 +428,9 @@ object BuiltinCommands
|
|||
}
|
||||
}
|
||||
def lastGrep = Command(LastGrepCommand, lastGrepBrief, lastGrepDetailed)(lastGrepParser) {
|
||||
case (s, (pattern,Some(sk))) =>
|
||||
case (s, (pattern,Some(sks))) =>
|
||||
val (str, ref, display) = extractLast(s)
|
||||
Output.lastGrep(sk, str, str.streams(s), pattern, printLast(s))(display)
|
||||
Output.lastGrep(sks, str.streams(s), pattern, printLast(s))(display)
|
||||
keepLastLog(s)
|
||||
case (s, (pattern, None)) =>
|
||||
for(logFile <- lastLogFile(s)) yield
|
||||
|
|
@ -460,13 +461,17 @@ object BuiltinCommands
|
|||
val keyMap = Project.structure(s).index.keyMap
|
||||
token(Space ~> (ID !!! "Expected key" examples keyMap.keySet)) flatMap { key => Act.getKey(keyMap, key, idFun) }
|
||||
}
|
||||
|
||||
val spacedKeyParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.scopedKeyParser(s))
|
||||
val optSpacedKeyParser = (s: State) => spacedKeyParser(s).?
|
||||
def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "<pattern>")) ~ optSpacedKeyParser(s))
|
||||
def last = Command(LastCommand, lastBrief, lastDetailed)(optSpacedKeyParser) {
|
||||
case (s,Some(sk)) =>
|
||||
val spacedAggregatedParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.aggregatedKeyParser(s))
|
||||
val aggregatedKeyValueParser: State => Parser[Option[AnyKeys]] =
|
||||
(s: State) => spacedAggregatedParser(s).map(x => Act.keyValues(s)(x) ).?
|
||||
|
||||
def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "<pattern>")) ~ aggregatedKeyValueParser(s))
|
||||
def last = Command(LastCommand, lastBrief, lastDetailed)(aggregatedKeyValueParser) {
|
||||
case (s,Some(sks)) =>
|
||||
val (str, ref, display) = extractLast(s)
|
||||
Output.last(sk, str, str.streams(s), printLast(s))(display)
|
||||
Output.last(sks, str.streams(s), printLast(s))(display)
|
||||
keepLastLog(s)
|
||||
case (s, None) =>
|
||||
for(logFile <- lastLogFile(s)) yield
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ package sbt
|
|||
import java.io.File
|
||||
import Keys.{Streams, TaskStreams}
|
||||
import Project.ScopedKey
|
||||
import Load.BuildStructure
|
||||
import Aggregation.{getTasks, KeyValue}
|
||||
import Aggregation.{KeyValue, Values}
|
||||
import Types.idFun
|
||||
import annotation.tailrec
|
||||
import scala.Console.{BOLD, RESET}
|
||||
|
|
@ -30,16 +29,16 @@ object Output
|
|||
}
|
||||
final val DefaultTail = "> "
|
||||
|
||||
def last(key: ScopedKey[_], structure: BuildStructure, streams: Streams, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit =
|
||||
printLines( flatLines(lastLines(key, structure, streams))(idFun) )
|
||||
def last(keys: Values[_], streams: Streams, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit =
|
||||
printLines( flatLines(lastLines(keys, streams))(idFun) )
|
||||
|
||||
def last(file: File, printLines: Seq[String] => Unit, tailDelim: String = DefaultTail): Unit =
|
||||
printLines(tailLines(file, tailDelim))
|
||||
|
||||
def lastGrep(key: ScopedKey[_], structure: BuildStructure, streams: Streams, patternString: String, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit =
|
||||
def lastGrep(keys: Values[_], streams: Streams, patternString: String, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit =
|
||||
{
|
||||
val pattern = Pattern compile patternString
|
||||
val lines = flatLines( lastLines(key, structure, streams) )(_ flatMap showMatches(pattern))
|
||||
val lines = flatLines( lastLines(keys, streams) )(_ flatMap showMatches(pattern))
|
||||
printLines( lines )
|
||||
}
|
||||
def lastGrep(file: File, patternString: String, printLines: Seq[String] => Unit, tailDelim: String = DefaultTail): Unit =
|
||||
|
|
@ -47,7 +46,7 @@ object Output
|
|||
def grep(lines: Seq[String], patternString: String): Seq[String] =
|
||||
lines flatMap showMatches(Pattern compile patternString)
|
||||
|
||||
def flatLines(outputs: Seq[KeyValue[Seq[String]]])(f: Seq[String] => Seq[String])(implicit display: Show[ScopedKey[_]]): Seq[String] =
|
||||
def flatLines(outputs: Values[Seq[String]])(f: Seq[String] => Seq[String])(implicit display: Show[ScopedKey[_]]): Seq[String] =
|
||||
{
|
||||
val single = outputs.size == 1
|
||||
outputs flatMap { case KeyValue(key, lines) =>
|
||||
|
|
@ -57,10 +56,9 @@ object Output
|
|||
}
|
||||
def bold(s: String) = if(ConsoleLogger.formatEnabled) BOLD + s + RESET else s
|
||||
|
||||
def lastLines(key: ScopedKey[_], structure: BuildStructure, streams: Streams): Seq[KeyValue[Seq[String]]] =
|
||||
def lastLines(keys: Values[_], streams: Streams): Values[Seq[String]] =
|
||||
{
|
||||
val aggregated = Aggregation.getTasks(key, structure, true)
|
||||
val outputs = aggregated map { case KeyValue(key, value) => KeyValue(key, lastLines(key, streams)) }
|
||||
val outputs = keys map { case KeyValue(key, value) => KeyValue(key, lastLines(key, streams)) }
|
||||
outputs.filterNot(_.value.isEmpty)
|
||||
}
|
||||
def lastLines(key: ScopedKey[_], mgr: Streams): Seq[String] = mgr.use(key) { s => IO.readLines(s.readText( Project.fillTaskAxis(key) )) }
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ final case class Extracted(structure: BuildStructure, session: SessionSettings,
|
|||
def runAggregated[T](key: TaskKey[T], state: State): State =
|
||||
{
|
||||
val rkey = resolve(key.scopedKey)
|
||||
val tasks = Aggregation.getTasks(rkey, structure, true)
|
||||
val keys = Resolve.aggregateDeps(rkey, ScopeMask(), structure.extra)
|
||||
val tasks = Act.keyValues(structure)(keys)
|
||||
Aggregation.runTasks(state, structure, tasks, Aggregation.Dummies(KNil, HNil), show = false )(showKey)
|
||||
}
|
||||
private[this] def resolve[T](key: ScopedKey[T]): ScopedKey[T] =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
package sbt
|
||||
|
||||
import java.net.URI
|
||||
import Load.LoadedBuildUnit
|
||||
|
||||
object Resolve
|
||||
{
|
||||
def apply(index: BuildUtil[_], current: ScopeAxis[Reference], key: AttributeKey[_], mask: ScopeMask): Scope => Scope =
|
||||
{
|
||||
val rs =
|
||||
resolveProject(current, mask) _ ::
|
||||
resolveExtra(mask) _ ::
|
||||
resolveTask(mask) _ ::
|
||||
resolveConfig(index, key, mask) _ ::
|
||||
Nil
|
||||
scope => (scope /: rs) { (s, f) => f(s) }
|
||||
}
|
||||
def resolveTask(mask: ScopeMask)(scope: Scope): Scope =
|
||||
if(mask.task) scope else scope.copy(task = Global)
|
||||
|
||||
def resolveProject(current: ScopeAxis[Reference], mask: ScopeMask)(scope: Scope): Scope =
|
||||
if(mask.project) scope else scope.copy(project = current)
|
||||
|
||||
def resolveExtra(mask: ScopeMask)(scope: Scope): Scope =
|
||||
if(mask.extra) scope else scope.copy(extra = Global)
|
||||
|
||||
def resolveConfig[P](index: BuildUtil[P], key: AttributeKey[_], mask: ScopeMask)(scope: Scope): Scope =
|
||||
if(mask.config)
|
||||
scope
|
||||
else
|
||||
{
|
||||
val (resolvedRef, proj) = scope.project match {
|
||||
case Select(ref) =>
|
||||
val r = index resolveRef ref
|
||||
(Some(r), index.projectFor(r))
|
||||
case Global | This =>
|
||||
(None, index.rootProject(index.root))
|
||||
}
|
||||
val task = scope.task.toOption
|
||||
val keyIndex = index.keyIndex
|
||||
val definesKey = (c: ScopeAxis[ConfigKey]) => keyIndex.keys(resolvedRef, c.toOption.map(_.name), task) contains key.label
|
||||
val projectConfigs = index.configurations(proj).map(ck => Select(ck))
|
||||
val config: ScopeAxis[ConfigKey] = (Global +: projectConfigs) find definesKey getOrElse Global
|
||||
scope.copy(config = config)
|
||||
}
|
||||
|
||||
import Load.BuildStructure
|
||||
import Project.ScopedKey
|
||||
|
||||
def projectAggregate[Proj](proj: Option[Reference], extra: BuildUtil[Proj], reverse: Boolean): Seq[ProjectRef] =
|
||||
{
|
||||
val resRef = proj.map(p => extra.projectRefFor(extra.resolveRef(p)))
|
||||
resRef.toList.flatMap(ref =>
|
||||
if(reverse) extra.aggregates.reverse(ref) else extra.aggregates.forward(ref)
|
||||
)
|
||||
}
|
||||
|
||||
def aggregateDeps[T, Proj](key: ScopedKey[T], rawMask: ScopeMask, extra: BuildUtil[Proj], reverse: Boolean = false): Seq[ScopedKey[T]] =
|
||||
{
|
||||
val mask = rawMask.copy(project = true)
|
||||
Dag.topologicalSort(key) { k =>
|
||||
val kref = k.scope.project
|
||||
for( ref <- projectAggregate(kref.toOption, extra, reverse)) yield
|
||||
{
|
||||
val toResolve = k.scope.copy(project = Select(ref))
|
||||
val resolved = apply(extra, Global, k.key, mask)(toResolve)
|
||||
ScopedKey(resolved, k.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def aggregates(units: Map[URI, LoadedBuildUnit]): Relation[ProjectRef, ProjectRef] =
|
||||
{
|
||||
val depPairs =
|
||||
for {
|
||||
(uri, unit) <- units.toIterable
|
||||
project <- unit.defined.values
|
||||
ref = ProjectRef(uri, project.id)
|
||||
agg <- project.aggregate
|
||||
} yield
|
||||
(ref, agg)
|
||||
Relation.empty ++ depPairs
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ object ParseKey extends Properties("Key parser test")
|
|||
import skm.{structure, key}
|
||||
val mask = ScopeMask(config = false)
|
||||
val string = Project.displayMasked(key, mask)
|
||||
val resolvedConfig = resolveConfig(structure, key.scope, key.key, mask).config
|
||||
val resolvedConfig = Resolve.resolveConfig(structure.extra, key.key, mask)(key.scope).config
|
||||
|
||||
("Key: " + Project.displayFull(key)) |:
|
||||
("Mask: " + mask) |:
|
||||
|
|
@ -85,37 +85,7 @@ object ParseKey extends Properties("Key parser test")
|
|||
final class StructureKeyMask(val structure: Structure, val key: ScopedKey[_], val mask: ScopeMask)
|
||||
|
||||
def resolve(structure: Structure, key: ScopedKey[_], mask: ScopeMask): ScopedKey[_] =
|
||||
{
|
||||
val scope = (key.scope /: List(resolveProject _, resolveExtra _, resolveTask _, resolveConfig _)) { (scope, f) =>
|
||||
f(structure, scope, key.key, mask)
|
||||
}
|
||||
ScopedKey(scope, key.key)
|
||||
}
|
||||
def resolveTask(structure: Structure, scope: Scope, key: AttributeKey[_], mask: ScopeMask): Scope =
|
||||
if(mask.task)
|
||||
scope
|
||||
else
|
||||
scope.copy(task = Global)
|
||||
|
||||
def resolveProject(structure: Structure, scope: Scope, key: AttributeKey[_], mask: ScopeMask): Scope =
|
||||
if(mask.project) scope else scope.copy(project = Select(structure.current))
|
||||
def resolveExtra(structure: Structure, scope: Scope, key: AttributeKey[_], mask: ScopeMask): Scope =
|
||||
if(mask.extra) scope else scope.copy(extra = Global)
|
||||
def resolveConfig(structure: Structure, scope: Scope, key: AttributeKey[_], mask: ScopeMask): Scope =
|
||||
if(mask.config)
|
||||
scope
|
||||
else
|
||||
{
|
||||
val env = structure.env
|
||||
val resolved = env.resolveAxis(scope.project)
|
||||
val project = env.projectFor( resolved )
|
||||
val ref = scope.project.toOption.map(env.resolve)
|
||||
val task = scope.task.toOption
|
||||
val projectConfigs = project.configurations.map(c => Select(ConfigKey(c.name)))
|
||||
val definesKey = (c: ScopeAxis[ConfigKey]) => structure.keyIndex.keys(ref, c.toOption.map(_.name), task) contains key.label
|
||||
val config = (Global +: projectConfigs) find definesKey getOrElse Global
|
||||
scope.copy(config = config)
|
||||
}
|
||||
ScopedKey(Resolve(structure.extra, Select(structure.current), key.key, mask)(key.scope), key.key)
|
||||
|
||||
def parseExpected(structure: Structure, s: String, expected: ScopedKey[_], mask: ScopeMask): Prop =
|
||||
("Expected: " + Project.displayFull(expected)) |:
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ object TestBuild
|
|||
(Scope.display(scope, "<key>"), showKeys(map))
|
||||
scopeStrings.toSeq.sorted.map(t => t._1 + t._2).mkString("\n\t")
|
||||
}
|
||||
val extra: BuildUtil[Proj] =
|
||||
{
|
||||
val getp = (build: URI, project: String) => env.buildMap(build).projectMap(project)
|
||||
new BuildUtil(keyIndex, env.root.uri, env.rootProject, getp, _.configurations.map(c => ConfigKey(c.name)), const(Nil))
|
||||
}
|
||||
|
||||
lazy val allAttributeKeys: Set[AttributeKey[_]] = data.data.values.flatMap(_.keys).toSet
|
||||
lazy val (taskAxes, globalTaskAxis, onlyTaskAxis, multiTaskAxis) =
|
||||
{
|
||||
|
|
@ -94,11 +100,6 @@ object TestBuild
|
|||
val taskMap = mapBy(tasks)(getKey)
|
||||
def project(ref: ProjectRef) = buildMap(ref.build).projectMap(ref.project)
|
||||
def projectFor(ref: ResolvedReference) = ref match { case pr: ProjectRef => project(pr); case BuildRef(uri) => buildMap(uri).root }
|
||||
def resolveAxis(ref: ScopeAxis[Reference]): ResolvedReference =
|
||||
{
|
||||
val rootRef = BuildRef(root.uri)
|
||||
ref.foldStrict(resolve, rootRef, rootRef)
|
||||
}
|
||||
|
||||
lazy val allProjects = builds.flatMap(_.allProjects)
|
||||
def rootProject(uri: URI): String = buildMap(uri).root.id
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ object Relation
|
|||
make(forward, reverse)
|
||||
}
|
||||
|
||||
|
||||
private[sbt] def remove[X,Y](map: M[X,Y], from: X, to: Y): M[X,Y] =
|
||||
map.get(from) match {
|
||||
case Some(tos) =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue