From be94b25d680efd2ad2e7b5f0ab79c242b376922a Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 15 Mar 2019 11:09:29 -0700 Subject: [PATCH] Add Event trait to FileCacheEntry Rather than exposing the FileEventMonitor.Event types, which are under active development in the io repo, I am adding a new event trait to FileCacheEntry. This trait doesn't expose any internal implementation details. --- main-command/src/main/scala/sbt/Watched.scala | 32 ++++++++++--------- .../scala/sbt/internal/FileCacheEntry.scala | 32 +++++++++++++++++-- .../src/test/scala/sbt/WatchedSpec.scala | 5 ++- main/src/main/scala/sbt/Keys.scala | 3 +- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 1af65fc2d..00ce14a05 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -23,7 +23,6 @@ import sbt.internal.util.Types.const import sbt.internal.util.complete.{ DefaultParsers, Parser } import sbt.internal.util.{ AttributeKey, JLine } import sbt.internal.{ FileCacheEntry, LegacyWatched } -import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update } import sbt.io._ import sbt.util.{ Level, Logger } @@ -145,13 +144,14 @@ object Watched { private[sbt] def onEvent( sources: Seq[WatchSource], projectSources: Seq[WatchSource] - ): Event[FileCacheEntry] => Watched.Action = + ): FileCacheEntry.Event => Watched.Action = event => - if (sources.exists(_.accept(event.entry.typedPath.toPath))) Watched.Trigger - else if (projectSources.exists(_.accept(event.entry.typedPath.toPath))) event match { - case Update(prev, cur, _) if prev.value != cur.value => Reload - case _: Creation[_] | _: Deletion[_] => Reload - case _ => Ignore + if (sources.exists(_.accept(event.path))) Watched.Trigger + else if (projectSources.exists(_.accept(event.path))) { + (event.previous, event.current) match { + case (Some(p), Some(c)) => if (c == p) Watched.Ignore else Watched.Reload + case _ => Watched.Trigger + } } else Ignore private[this] val reRun = if (isWin) "" else " or 'r' to re-run the command" @@ -333,7 +333,9 @@ object Watched { case action @ (CancelWatch | HandleError | Reload | _: Custom) => action case Trigger => Trigger case _ => - val events = config.fileEventMonitor.poll(10.millis) + val events = config.fileEventMonitor + .poll(10.millis) + .map(new FileCacheEntry.EventImpl(_)) val next = events match { case Seq() => (Ignore, None) case Seq(head, tail @ _*) => @@ -362,14 +364,14 @@ object Watched { if (action == HandleError) "error" else if (action.isInstanceOf[Custom]) action.toString else "cancellation" - logger.debug(s"Stopping watch due to $cause from ${event.entry.typedPath.toPath}") + logger.debug(s"Stopping watch due to $cause from ${event.path}") action case (Trigger, Some(event)) => - logger.debug(s"Triggered by ${event.entry.typedPath.toPath}") - config.triggeredMessage(event.entry.typedPath.toPath, count).foreach(info) + logger.debug(s"Triggered by ${event.path}") + config.triggeredMessage(event.path, count).foreach(info) Trigger case (Reload, Some(event)) => - logger.info(s"Reload triggered by ${event.entry.typedPath.toPath}") + logger.info(s"Reload triggered by ${event.path}") Reload case _ => nextAction() @@ -481,7 +483,7 @@ trait WatchConfig { * @param event the detected sbt.io.FileEventMonitor.Event. * @return the next [[Watched.Action Action]] to run. */ - def onWatchEvent(event: Event[FileCacheEntry]): Watched.Action + def onWatchEvent(event: FileCacheEntry.Event): Watched.Action /** * Transforms the state after the watch terminates. @@ -540,7 +542,7 @@ object WatchConfig { fileEventMonitor: FileEventMonitor[FileCacheEntry], handleInput: InputStream => Watched.Action, preWatch: (Int, Boolean) => Watched.Action, - onWatchEvent: Event[FileCacheEntry] => Watched.Action, + onWatchEvent: FileCacheEntry.Event => Watched.Action, onWatchTerminated: (Watched.Action, String, State) => State, triggeredMessage: (Path, Int) => Option[String], watchingMessage: Int => Option[String] @@ -559,7 +561,7 @@ object WatchConfig { override def handleInput(inputStream: InputStream): Watched.Action = hi(inputStream) override def preWatch(count: Int, lastResult: Boolean): Watched.Action = pw(count, lastResult) - override def onWatchEvent(event: Event[FileCacheEntry]): Watched.Action = owe(event) + override def onWatchEvent(event: FileCacheEntry.Event): Watched.Action = owe(event) override def onWatchTerminated(action: Watched.Action, command: String, state: State): State = owt(action, command, state) override def triggeredMessage(path: Path, count: Int): Option[String] = diff --git a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala b/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala index f7c0e71a4..19c055929 100644 --- a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala +++ b/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala @@ -8,10 +8,12 @@ package sbt package internal import java.lang +import java.nio.file.Path import java.util.Optional import sbt.internal.inc.{ EmptyStamp, LastModified, Stamp } -import sbt.io.TypedPath +import sbt.io.FileEventMonitor.{ Creation, Deletion, Update } +import sbt.io.{ FileEventMonitor, TypedPath } import xsbti.compile.analysis.{ Stamp => XStamp } /** @@ -23,7 +25,33 @@ trait FileCacheEntry { def lastModified: Option[Long] } object FileCacheEntry { - def default(typedPath: TypedPath): FileCacheEntry = + trait Event { + def path: Path + def previous: Option[FileCacheEntry] + def current: Option[FileCacheEntry] + } + private[sbt] class EventImpl(event: FileEventMonitor.Event[FileCacheEntry]) extends Event { + override def path: Path = event.entry.typedPath.toPath + override def previous: Option[FileCacheEntry] = event match { + case Deletion(entry, _) => entry.value.toOption + case Update(previous, _, _) => previous.value.toOption + case _ => None + } + override def current: Option[FileCacheEntry] = event match { + case Creation(entry, _) => entry.value.toOption + case Update(_, current, _) => current.value.toOption + case _ => None + } + override def equals(o: Any): Boolean = o match { + case that: Event => + this.path == that.path && this.previous == that.previous && this.current == that.current + case _ => false + } + override def hashCode(): Int = + ((path.hashCode * 31) ^ previous.hashCode() * 31) ^ current.hashCode() + override def toString: String = s"Event($path, $previous, $current)" + } + private[sbt] def default(typedPath: TypedPath): FileCacheEntry = DelegateFileCacheEntry(Stamped.converter(typedPath)) private[sbt] implicit class FileCacheEntryOps(val e: FileCacheEntry) extends AnyVal { private[sbt] def stamp: XStamp = e match { diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index f0555020b..60f73b7da 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -15,7 +15,6 @@ import org.scalatest.{ FlatSpec, Matchers } import sbt.Watched._ import sbt.WatchedSpec._ import sbt.internal.FileCacheEntry -import sbt.io.FileEventMonitor.Event import sbt.io._ import sbt.io.syntax._ import sbt.util.Logger @@ -31,7 +30,7 @@ class WatchedSpec extends FlatSpec with Matchers { logger: Logger = NullLogger, handleInput: InputStream => Action = _ => Ignore, preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch, - onWatchEvent: Event[FileCacheEntry] => Action = _ => Ignore, + onWatchEvent: FileCacheEntry.Event => Action = _ => Ignore, triggeredMessage: (Path, Int) => Option[String] = (_, _) => None, watchingMessage: Int => Option[String] = _ => None ): WatchConfig = { @@ -87,7 +86,7 @@ class WatchedSpec extends FlatSpec with Matchers { val config = Defaults.config( globs = Seq(realDir ** AllPassFilter), preWatch = (count, _) => if (count == 2) CancelWatch else Ignore, - onWatchEvent = e => if (e.entry.typedPath.toPath == foo) Trigger else Ignore, + onWatchEvent = e => if (e.path == foo) Trigger else Ignore, triggeredMessage = (tp, _) => { queue += tp; None }, watchingMessage = _ => { Files.createFile(bar); Thread.sleep(5); Files.createFile(foo); None } ) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 6ad5b58bf..62033ed54 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -23,7 +23,6 @@ import sbt.internal.io.WatchState import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.server.ServerHandler import sbt.internal.util.{ AttributeKey, SourcePosition } -import sbt.io.FileEventMonitor.Event import sbt.io._ import sbt.librarymanagement.Configurations.CompilerPlugin import sbt.librarymanagement.LibraryManagementCodec._ @@ -102,7 +101,7 @@ object Keys { val watchConfig = taskKey[WatchConfig]("The configuration for continuous execution.").withRank(BMinusSetting) val watchLogger = taskKey[Logger]("A logger that reports watch events.").withRank(DSetting) val watchHandleInput = settingKey[InputStream => Watched.Action]("Function that is periodically invoked to determine if the continous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands.").withRank(BMinusSetting) - val watchOnEvent = taskKey[Event[FileCacheEntry] => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting) + val watchOnEvent = taskKey[FileCacheEntry.Event => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting) val watchOnTermination = taskKey[(Watched.Action, String, State) => State]("Transforms the input state after the continuous build completes.").withRank(BMinusSetting) val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting) val watchProjectSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources for the sbt meta project to watch to trigger a reload.").withRank(CSetting)