From 7250a28ac4c14f57cfdbaff1339bfcefca5f0525 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 29 Dec 2017 00:48:11 -0800 Subject: WIP Migrating `Target` name/pickler generation from the `Mirror` discovery into the `Target` definitions themselves. Haven't deleted the old/unused `Mirror` data structures yet... --- core/src/main/scala/mill/define/Task.scala | 73 +++++++++++++++------- core/src/main/scala/mill/discover/Discovered.scala | 4 +- core/src/main/scala/mill/discover/Mirror.scala | 3 +- core/src/main/scala/mill/eval/Evaluator.scala | 55 +++++++++++----- core/src/main/scala/mill/main/RunScript.scala | 5 +- .../test/scala/mill/define/MacroErrorTests.scala | 2 +- .../test/scala/mill/discover/DiscoveredTests.scala | 11 ++-- .../src/test/scala/mill/eval/EvaluationTests.scala | 2 +- core/src/test/scala/mill/util/TestGraphs.scala | 38 +++++------ core/src/test/scala/mill/util/TestUtil.scala | 44 +++++++++---- 10 files changed, 157 insertions(+), 80 deletions(-) (limited to 'core/src') diff --git a/core/src/main/scala/mill/define/Task.scala b/core/src/main/scala/mill/define/Task.scala index 4dc5f692..a5007d45 100644 --- a/core/src/main/scala/mill/define/Task.scala +++ b/core/src/main/scala/mill/define/Task.scala @@ -3,7 +3,7 @@ package mill.define import mill.define.Applicative.Applyable import mill.eval.{PathRef, Result} import mill.util.Ctx - +import upickle.default.{Reader => R, Writer => W, ReadWriter => RW} import scala.language.experimental.macros import scala.reflect.macros.blackbox.Context @@ -32,34 +32,42 @@ abstract class Task[+T] extends Task.Ops[T] with Applyable[Task, T]{ def self = this } -trait Target[+T] extends Task[T]{ +trait NamedTask[+T] extends Task[T]{ + def owner: Task.Module + def name: String +} +trait Target[+T] extends NamedTask[T]{ override def asTarget = Some(this) + def enclosing: String + def readWrite: RW[_] } object Target extends TargetGenerated with Applicative.Applyer[Task, Task, Result, Ctx] { - implicit def apply[T](t: T): Target[T] = macro targetImpl[T] + implicit def apply[T](t: T)(implicit r: R[T], w: W[T]): Target[T] = macro targetImpl[T] - implicit def apply[T](t: Result[T]): Target[T] = macro targetResultImpl[T] + implicit def apply[T](t: Result[T])(implicit r: R[T], w: W[T]): Target[T] = macro targetResultImpl[T] - def apply[T](t: Task[T]): Target[T] = macro targetTaskImpl[T] + def apply[T](t: Task[T])(implicit r: R[T], w: W[T]): Target[T] = macro targetTaskImpl[T] def command[T](t: Result[T]): Command[T] = macro commandImpl[T] def source(path: ammonite.ops.Path) = new Source(path) - def command[T](t: Task[T]): Command[T] = new Command(t) + def command[T](t: Task[T])(implicit c: Caller[Task.Module], n: sourcecode.Name): Command[T] = new Command(t, c.value, n.value) def task[T](t: Result[T]): Task[T] = macro Applicative.impl[Task, T, Ctx] def task[T](t: Task[T]): Task[T] = t - def persistent[T](t: Result[T]): Target[T] = macro persistentImpl[T] - def persistentImpl[T: c.WeakTypeTag](c: Context)(t: c.Expr[T]): c.Expr[Persistent[T]] = { + def persistent[T](t: Result[T])(implicit r: R[T], w: W[T]): Target[T] = macro persistentImpl[T] + def persistentImpl[T: c.WeakTypeTag](c: Context) + (t: c.Expr[T]) + (r: c.Expr[R[T]], w: c.Expr[W[T]]): c.Expr[Persistent[T]] = { import c.universe._ c.Expr[Persistent[T]]( mill.plugin.Cacher.wrapCached(c)( - q"new ${weakTypeOf[Persistent[T]]}(${Applicative.impl[Task, T, Ctx](c)(t).tree})" + q"new ${weakTypeOf[Persistent[T]]}(${Applicative.impl[Task, T, Ctx](c)(t).tree}, _root_.sourcecode.Enclosing(), _root_.mill.define.Caller[mill.define.Task.Module](), _root_.sourcecode.Name(), upickle.default.ReadWriter($w.write, $r.read))" ) ) } @@ -67,32 +75,38 @@ object Target extends TargetGenerated with Applicative.Applyer[Task, Task, Resul import c.universe._ c.Expr[Command[T]]( - q"new ${weakTypeOf[Command[T]]}(${Applicative.impl[Task, T, Ctx](c)(t).tree})" + q"new ${weakTypeOf[Command[T]]}(${Applicative.impl[Task, T, Ctx](c)(t).tree}, _root_.mill.define.Caller[mill.define.Task.Module](), _root_.sourcecode.Name())" ) } - def targetTaskImpl[T: c.WeakTypeTag](c: Context)(t: c.Expr[Task[T]]): c.Expr[Target[T]] = { + def targetTaskImpl[T: c.WeakTypeTag](c: Context) + (t: c.Expr[Task[T]]) + (r: c.Expr[R[T]], w: c.Expr[W[T]]): c.Expr[Target[T]] = { import c.universe._ c.Expr[Target[T]]( mill.plugin.Cacher.wrapCached(c)( - q"new ${weakTypeOf[TargetImpl[T]]}($t, _root_.sourcecode.Enclosing())" + q"new ${weakTypeOf[TargetImpl[T]]}($t, _root_.sourcecode.Enclosing(), _root_.mill.define.Caller[mill.define.Task.Module](), _root_.sourcecode.Name(), upickle.default.ReadWriter($w.write, $r.read))" ) ) } - def targetImpl[T: c.WeakTypeTag](c: Context)(t: c.Expr[T]): c.Expr[Target[T]] = { + def targetImpl[T: c.WeakTypeTag](c: Context) + (t: c.Expr[T]) + (r: c.Expr[R[T]], w: c.Expr[W[T]]): c.Expr[Target[T]] = { import c.universe._ c.Expr[Target[T]]( mill.plugin.Cacher.wrapCached(c)( - q"new ${weakTypeOf[TargetImpl[T]]}(${Applicative.impl0[Task, T, Ctx](c)(q"mill.eval.Result.Success($t)").tree}, _root_.sourcecode.Enclosing())" + q"new ${weakTypeOf[TargetImpl[T]]}(${Applicative.impl0[Task, T, Ctx](c)(q"mill.eval.Result.Success($t)").tree}, _root_.sourcecode.Enclosing(), _root_.mill.define.Caller[mill.define.Task.Module](), _root_.sourcecode.Name(), upickle.default.ReadWriter($w.write, $r.read))" ) ) } - def targetResultImpl[T: c.WeakTypeTag](c: Context)(t: c.Expr[Result[T]]): c.Expr[Target[T]] = { + def targetResultImpl[T: c.WeakTypeTag](c: Context) + (t: c.Expr[Result[T]]) + (r: c.Expr[R[T]], w: c.Expr[W[T]]): c.Expr[Target[T]] = { import c.universe._ c.Expr[Target[T]]( mill.plugin.Cacher.wrapCached(c)( - q"new ${weakTypeOf[TargetImpl[T]]}(${Applicative.impl0[Task, T, Ctx](c)(t.tree).tree}, _root_.sourcecode.Enclosing())" + q"new ${weakTypeOf[TargetImpl[T]]}(${Applicative.impl0[Task, T, Ctx](c)(t.tree).tree}, _root_.sourcecode.Enclosing(), _root_.mill.define.Caller[mill.define.Task.Module](), _root_.sourcecode.Name(), upickle.default.ReadWriter($w.write, $r.read))" ) ) } @@ -109,19 +123,36 @@ object Target extends TargetGenerated with Applicative.Applyer[Task, Task, Resul def zip[A](a: Task[A]) = a.map(Tuple1(_)) def zip[A, B](a: Task[A], b: Task[B]) = a.zip(b) } -class TargetImpl[+T](t: Task[T], enclosing: String) extends Target[T] { +case class Caller[A](value: A) +object Caller { + def apply[T]()(implicit c: Caller[T]) = c.value + implicit def generate[T]: Caller[T] = macro impl[T] + def impl[T: c.WeakTypeTag](c: Context): c.Tree = { + import c.universe._ + q"new _root_.mill.define.Caller[${weakTypeOf[T]}](this)" + } +} + +class TargetImpl[+T](t: Task[T], + val enclosing: String, + val owner: Task.Module, + val name: String, + val readWrite: RW[_]) extends Target[T] { val inputs = Seq(t) def evaluate(args: Ctx) = args[T](0) override def toString = enclosing + "@" + Integer.toHexString(System.identityHashCode(this)) } -class Command[+T](t: Task[T]) extends Task[T] { +class Command[+T](t: Task[T], val owner: Task.Module, val name: String) extends NamedTask[T] { val inputs = Seq(t) def evaluate(args: Ctx) = args[T](0) override def asCommand = Some(this) } -class Persistent[+T](t: Task[T]) extends Target[T] { - val inputs = Seq(t) - def evaluate(args: Ctx) = args[T](0) +class Persistent[+T](t: Task[T], + enclosing: String, + owner: Task.Module, + name: String, + readWrite: RW[_]) + extends TargetImpl[T](t, enclosing, owner, name, readWrite) { override def flushDest = false override def asPersistent = Some(this) } diff --git a/core/src/main/scala/mill/discover/Discovered.scala b/core/src/main/scala/mill/discover/Discovered.scala index a8d91f1d..8824a341 100644 --- a/core/src/main/scala/mill/discover/Discovered.scala +++ b/core/src/main/scala/mill/discover/Discovered.scala @@ -19,7 +19,7 @@ class Discovered[T](val mirror: Mirror[T, T]){ } def mapping(t: T) = { Discovered.Mapping( - targets(t).map(x => x.target -> x).toMap[Target[Any], LabelledTarget[_]], + targets(t).map(x => x.target -> x).toMap[Task[Any], LabelledTarget[_]], mirror, t ) @@ -31,7 +31,7 @@ object Discovered { // Magically injected by the `Evaluator`, rather than being constructed here def make() = ??? } - case class Mapping[T](value: Map[Target[Any], LabelledTarget[_]], + case class Mapping[T](value: Map[Task[Any], LabelledTarget[_]], mirror: Mirror[T, T], base: T) diff --git a/core/src/main/scala/mill/discover/Mirror.scala b/core/src/main/scala/mill/discover/Mirror.scala index 926d9669..7fa9736b 100644 --- a/core/src/main/scala/mill/discover/Mirror.scala +++ b/core/src/main/scala/mill/discover/Mirror.scala @@ -56,10 +56,11 @@ object Mirror{ rec(Nil, hierarchy) } + /** * A target after being materialized in a concrete build */ - case class LabelledTarget[V](target: Target[V], + case class LabelledTarget[V](target: Task[V], format: upickle.default.ReadWriter[V], segments: Seq[Segment]) diff --git a/core/src/main/scala/mill/eval/Evaluator.scala b/core/src/main/scala/mill/eval/Evaluator.scala index 96ee1b6e..130dbcbc 100644 --- a/core/src/main/scala/mill/eval/Evaluator.scala +++ b/core/src/main/scala/mill/eval/Evaluator.scala @@ -4,30 +4,48 @@ import java.net.URLClassLoader import ammonite.ops._ import ammonite.runtime.SpecialClassLoader -import mill.define.{Graph, Target, Task, Worker} +import mill.define._ import mill.discover.{Discovered, Mirror} -import mill.discover.Mirror.LabelledTarget -import mill.discover.Mirror.Segment.{Cross, Label} +import mill.discover.Mirror.{LabelledTarget, Segment} import mill.util import mill.util._ import scala.collection.mutable - +case class Labelled[T](target: Task[T], + format: Option[upickle.default.ReadWriter[T]], + segments: Seq[Segment]) class Evaluator[T](val workspacePath: Path, val mapping: Discovered.Mapping[T], log: Logger, val classLoaderSig: Seq[(Path, Long)] = Evaluator.classLoaderSig){ - val labeling = mapping.value + + val moduleMapping = Mirror.traverse(mapping.base, mapping.mirror){ (mirror, segmentsRev) => + val resolvedNode = mirror.node( + mapping.base, + segmentsRev.reverse.map{case Mirror.Segment.Cross(vs) => vs.toList case _ => Nil}.toList + ) + Seq(resolvedNode -> segmentsRev.reverse) + }.toMap + val workerCache = mutable.Map.empty[Ctx.Loader[_], Any] workerCache(Discovered.Mapping) = mapping def evaluate(goals: OSet[Task[_]]): Evaluator.Results = { mkdir(workspacePath) + LabelledTarget val transitive = Graph.transitiveTargets(goals) val topoSorted = Graph.topoSorted(transitive) val sortedGroups = Graph.groupAroundImportantTargets(topoSorted){ - case t: Target[_] if labeling.contains(t) || goals.contains(t) => Right(labeling(t)) + case t: NamedTask[Any] if moduleMapping.contains(t.owner) => + Right(Labelled( + t, + t match{ + case t: Target[Any] => Some(t.readWrite.asInstanceOf[upickle.default.ReadWriter[Any]]) + case _ => None + }, + moduleMapping(t.owner) :+ Segment.Label(t.name) + )) case t if goals.contains(t) => Left(t) } @@ -43,7 +61,7 @@ class Evaluator[T](val workspacePath: Path, } - val failing = new util.MultiBiMap.Mutable[Either[Task[_], LabelledTarget[_]], Result.Failing] + val failing = new util.MultiBiMap.Mutable[Either[Task[_], Labelled[_]], Result.Failing] for((k, vs) <- sortedGroups.items()){ failing.addAll(k, vs.items.flatMap(results.get).collect{case f: Result.Failing => f}) } @@ -51,8 +69,7 @@ class Evaluator[T](val workspacePath: Path, } - - def evaluateGroupCached(terminal: Either[Task[_], LabelledTarget[_]], + def evaluateGroupCached(terminal: Either[Task[_], Labelled[_]], group: OSet[Task[_]], results: collection.Map[Task[_], Result[Any]]): (collection.Map[Task[_], Result[Any]], Seq[Task[_]]) = { @@ -78,7 +95,7 @@ class Evaluator[T](val workspacePath: Path, cached match{ case Some(terminalResult) => val newResults = mutable.LinkedHashMap.empty[Task[_], Result[Any]] - newResults(labelledTarget.target) = labelledTarget.format.read(terminalResult) + newResults(labelledTarget.target) = labelledTarget.format.get.read(terminalResult) (newResults, Nil) case _ => @@ -100,10 +117,12 @@ class Evaluator[T](val workspacePath: Path, case Result.Success(v) => val terminalResult = labelledTarget .format - .asInstanceOf[upickle.default.ReadWriter[Any]] - .write(v) + .asInstanceOf[Option[upickle.default.ReadWriter[Any]]] + .map(_.write(v)) - write.over(metadataPath, upickle.default.write(inputsHash -> terminalResult, indent = 4)) + for(t <- terminalResult){ + write.over(metadataPath, upickle.default.write(inputsHash -> t, indent = 4)) + } case _ => // Wipe out any cached metadata.mill.json file that exists, so // a following run won't look at the cached metadata file and @@ -206,7 +225,13 @@ class Evaluator[T](val workspacePath: Path, object Evaluator{ def resolveDestPaths(workspacePath: Path, t: LabelledTarget[_]): (Path, Path) = { - val segmentStrings = t.segments.flatMap{ + resolveDestPaths(workspacePath, t.segments) + } + def resolveDestPaths(workspacePath: Path, t: Labelled[_]): (Path, Path) = { + resolveDestPaths(workspacePath, t.segments) + } + def resolveDestPaths(workspacePath: Path, segments: Seq[Segment]): (Path, Path) = { + val segmentStrings = segments.flatMap{ case Mirror.Segment.Label(s) => Seq(s) case Mirror.Segment.Cross(values) => values.map(_.toString) } @@ -225,7 +250,7 @@ object Evaluator{ case class Results(rawValues: Seq[Result[Any]], evaluated: OSet[Task[_]], transitive: OSet[Task[_]], - failing: MultiBiMap[Either[Task[_], LabelledTarget[_]], Result.Failing]){ + failing: MultiBiMap[Either[Task[_], Labelled[_]], Result.Failing]){ def values = rawValues.collect{case Result.Success(v) => v} } } diff --git a/core/src/main/scala/mill/main/RunScript.scala b/core/src/main/scala/mill/main/RunScript.scala index f58cf5c0..3d41be2c 100644 --- a/core/src/main/scala/mill/main/RunScript.scala +++ b/core/src/main/scala/mill/main/RunScript.scala @@ -8,6 +8,7 @@ import ammonite.util.Util.CodeSource import ammonite.util.{Name, Res, Util} import mill.define import mill.define.Task +import mill.discover.Mirror.Segment import mill.discover.{Discovered, Mirror} import mill.eval.{Evaluator, Result} import mill.util.{OSet, PrintLogger} @@ -141,8 +142,8 @@ object RunScript{ val json = for(t <- Seq(target)) yield { t match { case t: mill.define.Target[_] => - for (labelled <- evaluator.labeling.get(t)) yield { - val jsonFile = Evaluator.resolveDestPaths(evaluator.workspacePath, labelled)._2 + for (segments <- evaluator.moduleMapping.get(t.owner)) yield { + val jsonFile = Evaluator.resolveDestPaths(evaluator.workspacePath, segments :+ Segment.Label(t.name))._2 val metadata = upickle.json.read(jsonFile.toIO) metadata(1) } diff --git a/core/src/test/scala/mill/define/MacroErrorTests.scala b/core/src/test/scala/mill/define/MacroErrorTests.scala index b0d687fe..7fc79f62 100644 --- a/core/src/test/scala/mill/define/MacroErrorTests.scala +++ b/core/src/test/scala/mill/define/MacroErrorTests.scala @@ -72,7 +72,7 @@ object MacroErrorTests extends TestSuite{ def down = T{ TestUtil.test(left, right) } } """) - assert(borkedCachedDiamond1.msg.contains("must be defs")) + assert(borkedCachedDiamond1.msg.contains("required: mill.Module")) } } } diff --git a/core/src/test/scala/mill/discover/DiscoveredTests.scala b/core/src/test/scala/mill/discover/DiscoveredTests.scala index 0f73074f..31d6119e 100644 --- a/core/src/test/scala/mill/discover/DiscoveredTests.scala +++ b/core/src/test/scala/mill/discover/DiscoveredTests.scala @@ -49,7 +49,7 @@ object DiscoveredTests extends TestSuite{ } 'commands - { - object outer { + object outer extends mill.Module{ def hello() = T.command{ println("Hello") } @@ -89,14 +89,15 @@ object DiscoveredTests extends TestSuite{ 'compileError - { 'unserializableTarget - { + object outer extends Module { - def single = mill.T{ new InputStreamReader(System.in) } + val error = compileError("def single = mill.T{ new InputStreamReader(System.in) }") } - val error = compileError("Discovered.make[outer.type]") + assert( - error.msg.contains("uPickle does not know how to read"), - error.pos.contains("def single = mill.T{ new InputStreamReader(System.in) }") + outer.error.msg.contains("uPickle does not know how to read"), + outer.error.pos.contains("def single = mill.T{ new InputStreamReader(System.in) }") ) } diff --git a/core/src/test/scala/mill/eval/EvaluationTests.scala b/core/src/test/scala/mill/eval/EvaluationTests.scala index 8b13987b..2b40724a 100644 --- a/core/src/test/scala/mill/eval/EvaluationTests.scala +++ b/core/src/test/scala/mill/eval/EvaluationTests.scala @@ -209,7 +209,7 @@ object EvaluationTests extends TestSuite{ var leftCount = 0 var rightCount = 0 var middleCount = 0 - def up = T{ test() } + def up = T{ test.anon() } def left = T.task{ leftCount += 1; up() + 1 } def middle = T.task{ middleCount += 1; 100 } def right = T{ rightCount += 1; 10000 } diff --git a/core/src/test/scala/mill/util/TestGraphs.scala b/core/src/test/scala/mill/util/TestGraphs.scala index a69221da..86d331bb 100644 --- a/core/src/test/scala/mill/util/TestGraphs.scala +++ b/core/src/test/scala/mill/util/TestGraphs.scala @@ -15,20 +15,20 @@ import mill.{Module, T} */ class TestGraphs(){ // single - object singleton { + object singleton extends Module{ val single = test() } // up---down - object pair { + object pair extends Module { val up = test() val down = test(up) } // up---o---down - object anonTriple{ + object anonTriple extends Module{ val up = test() - val down = test(test(up)) + val down = test(test.anon(up)) } // left @@ -36,7 +36,7 @@ class TestGraphs(){ // up down // \ / // right - object diamond{ + object diamond extends Module{ val up = test() val left = test(up) val right = test(up) @@ -48,9 +48,9 @@ class TestGraphs(){ // up down // \ / // o - object anonDiamond{ + object anonDiamond extends Module{ val up = test() - val down = test(test(up), test(up)) + val down = test(test.anon(up), test.anon(up)) } object defCachedDiamond extends Module{ @@ -68,7 +68,7 @@ class TestGraphs(){ def down = test(left, right) } - object borkedCachedDiamond3 { + object borkedCachedDiamond3 extends Module { def up = test() def left = test(up) def right = test(up) @@ -86,22 +86,22 @@ class TestGraphs(){ // o o---F---o // / / // o--B o - object bigSingleTerminal{ - val a = test(test(), test()) - val b = test(test()) + object bigSingleTerminal extends Module{ + val a = test(test.anon(), test.anon()) + val b = test(test.anon()) val e = { - val c = test(a) - val d = test(a) - test(test(test(), test(c)), test(test(c, test(d, b)))) + val c = test.anon(a) + val d = test.anon(a) + test(test.anon(test.anon(), test.anon(c)), test.anon(test.anon(c, test.anon(d, b)))) } - val f = test(test(test(), test(e))) + val f = test(test.anon(test.anon(), test.anon(e))) val i = { - val g = test() - val h = test(g, e) - test(test(g), test(test(h))) + val g = test.anon() + val h = test.anon(g, e) + test(test.anon(g), test.anon(test.anon(h))) } - val j = test(test(i), test(i, f), test(f)) + val j = test(test.anon(i), test.anon(i, f), test.anon(f)) } } object TestGraphs{ diff --git a/core/src/test/scala/mill/util/TestUtil.scala b/core/src/test/scala/mill/util/TestUtil.scala index a456b22b..8adbc87a 100644 --- a/core/src/test/scala/mill/util/TestUtil.scala +++ b/core/src/test/scala/mill/util/TestUtil.scala @@ -1,33 +1,51 @@ package mill.util -import mill.define.{Target, Task} +import mill.define.{Caller, Target, Task} import mill.eval.Result import utest.assert import scala.collection.mutable object TestUtil { - def test(inputs: Task[Int]*) = { - new Test(inputs, pure = inputs.nonEmpty) + object test{ + + def anon(inputs: Task[Int]*) = new Test(inputs) + def apply(inputs: Task[Int]*) + (implicit enclosing0: sourcecode.Enclosing, + owner0: Caller[mill.Module], + name0: sourcecode.Name)= { + new TestTarget(inputs, pure = inputs.nonEmpty) + } } - /** - * A dummy target that takes any number of inputs, and whose output can be - * controlled externally, so you can construct arbitrary dataflow graphs and - * test how changes propagate. - */ - class Test(override val inputs: Seq[Task[Int]], - val pure: Boolean) extends Target[Int]{ + class Test(val inputs: Seq[Task[Int]]) extends Task[Int]{ var counter = 0 var failure = Option.empty[String] var exception = Option.empty[Throwable] override def evaluate(args: Ctx) = { failure.map(Result.Failure) orElse - exception.map(Result.Exception) getOrElse - Result.Success(counter + args.args.map(_.asInstanceOf[Int]).sum) + exception.map(Result.Exception) getOrElse + Result.Success(counter + args.args.map(_.asInstanceOf[Int]).sum) } - override def sideHash = counter + failure.hashCode() + exception.hashCode() + } + /** + * A dummy target that takes any number of inputs, and whose output can be + * controlled externally, so you can construct arbitrary dataflow graphs and + * test how changes propagate. + */ + class TestTarget(inputs: Seq[Task[Int]], + val pure: Boolean) + (implicit enclosing0: sourcecode.Enclosing, + owner0: Caller[mill.Module], + name0: sourcecode.Name) + extends Test(inputs) with Target[Int]{ + val enclosing = enclosing0.value + val owner = owner0.value + val name = name0.value + val readWrite = upickle.default.IntRW + + } def checkTopological(targets: OSet[Task[_]]) = { val seen = mutable.Set.empty[Task[_]] -- cgit v1.2.3