Skip to content

Commit fb06636

Browse files
committed
Skip abandoned repos
1 parent f5a5a09 commit fb06636

File tree

13 files changed

+119
-12
lines changed

13 files changed

+119
-12
lines changed

modules/core/src/main/resources/default.scala-steward.conf

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// Changes to this file are therefore immediately visible to all
55
// Scala Steward instances.
66

7+
lastCommitMaxAge = "540 days"
8+
79
postUpdateHooks = [
810
{
911
groupId = "com.github.liancheng",

modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import org.scalasteward.core.forge.ForgeType.*
2525
import org.scalasteward.core.git.FileGitAlg.{dotdot, gitCmd}
2626
import org.scalasteward.core.io.process.{ProcessFailedException, SlurpOptions}
2727
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
28-
import org.scalasteward.core.util.Nel
28+
import org.scalasteward.core.util.{Nel, Timestamp}
29+
import scala.util.Try
2930

3031
final class FileGitAlg[F[_]](config: Config)(implicit
3132
fileAlg: FileAlg[F],
@@ -102,6 +103,11 @@ final class FileGitAlg[F[_]](config: Config)(implicit
102103
.handleError(_ => List.empty[String])
103104
.map(_.filter(_.nonEmpty))
104105

106+
override def getCommitDate(repo: File, sha1: Sha1): F[Timestamp] =
107+
git("show", "--no-patch", "--format=%ct", sha1.value.value)(repo)
108+
.flatMap(out => F.fromTry(Try(out.mkString.trim.toLong)))
109+
.map(Timestamp.fromEpochSecond)
110+
105111
override def hasConflicts(repo: File, branch: Branch, base: Branch): F[Boolean] = {
106112
val tryMerge = git_("merge", "--no-commit", "--no-ff", branch.name)(repo)
107113
val abortMerge = git_("merge", "--abort")(repo).attempt.void

modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import cats.{FlatMap, Monad}
2222
import org.http4s.Uri
2323
import org.scalasteward.core.application.Config
2424
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
25+
import org.scalasteward.core.util.Timestamp
2526

2627
trait GenGitAlg[F[_], Repo] {
2728
def add(repo: Repo, file: String): F[Unit]
@@ -57,6 +58,8 @@ trait GenGitAlg[F[_], Repo] {
5758

5859
def findFilesContaining(repo: Repo, string: String): F[List[String]]
5960

61+
def getCommitDate(repo: Repo, sha1: Sha1): F[Timestamp]
62+
6063
/** Returns `true` if merging `branch` into `base` results in merge conflicts. */
6164
def hasConflicts(repo: Repo, branch: Branch, base: Branch): F[Boolean]
6265

@@ -144,6 +147,9 @@ trait GenGitAlg[F[_], Repo] {
144147
override def findFilesContaining(repo: A, string: String): F[List[String]] =
145148
f(repo).flatMap(self.findFilesContaining(_, string))
146149

150+
override def getCommitDate(repo: A, sha1: Sha1): F[Timestamp] =
151+
f(repo).flatMap(self.getCommitDate(_, sha1))
152+
147153
override def hasConflicts(repo: A, branch: Branch, base: Branch): F[Boolean] =
148154
f(repo).flatMap(self.hasConflicts(_, branch, base))
149155

modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCache.scala

+2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import io.circe.generic.semiauto.*
2222
import org.scalasteward.core.data.{ArtifactId, DependencyInfo, GroupId, Scope}
2323
import org.scalasteward.core.git.Sha1
2424
import org.scalasteward.core.repoconfig.RepoConfig
25+
import org.scalasteward.core.util.Timestamp
2526

2627
final case class RepoCache(
2728
sha1: Sha1,
29+
commitDate: Timestamp,
2830
dependencyInfos: List[Scope[List[DependencyInfo]]],
2931
maybeRepoConfig: Option[RepoConfig],
3032
maybeRepoConfigParsingError: Option[String]

modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala

+19-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ import org.scalasteward.core.forge.data.RepoOut
2525
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeRepoAlg}
2626
import org.scalasteward.core.git.GitAlg
2727
import org.scalasteward.core.repoconfig.RepoConfigAlg
28+
import org.scalasteward.core.util.{dateTime, DateTimeAlg}
2829
import org.typelevel.log4cats.Logger
30+
import scala.util.control.NoStackTrace
2931

3032
final class RepoCacheAlg[F[_]](config: Config)(implicit
3133
buildToolDispatcher: BuildToolDispatcher[F],
34+
dateTimeAlg: DateTimeAlg[F],
3235
forgeApiAlg: ForgeApiAlg[F],
3336
forgeRepoAlg: ForgeRepoAlg[F],
3437
gitAlg: GitAlg[F],
@@ -50,6 +53,7 @@ final class RepoCacheAlg[F[_]](config: Config)(implicit
5053
data <- maybeCache
5154
.filter(_.sha1 === latestSha1)
5255
.fold(cloneAndRefreshCache(repo, repoOut))(supplementCache(repo, _).pure[F])
56+
_ <- throwIfAbandoned(data)
5357
} yield (data, repoOut)
5458

5559
private def supplementCache(repo: Repo, cache: RepoCache): RepoData =
@@ -68,7 +72,8 @@ final class RepoCacheAlg[F[_]](config: Config)(implicit
6872
private def computeCache(repo: Repo): F[RepoData] =
6973
for {
7074
branch <- gitAlg.currentBranch(repo)
71-
latestSha1 <- gitAlg.latestSha1(repo, branch)
75+
sha1 <- gitAlg.latestSha1(repo, branch)
76+
commitDate <- gitAlg.getCommitDate(repo, sha1)
7277
configParsingResult <- repoConfigAlg.readRepoConfig(repo)
7378
maybeConfig = configParsingResult.maybeRepoConfig
7479
maybeConfigParsingError = configParsingResult.maybeParsingError.map(_.getMessage)
@@ -77,9 +82,21 @@ final class RepoCacheAlg[F[_]](config: Config)(implicit
7782
dependencyInfos <-
7883
dependencies.traverse(_.traverse(_.traverse(gatherDependencyInfo(repo, _))))
7984
_ <- gitAlg.discardChanges(repo)
80-
cache = RepoCache(latestSha1, dependencyInfos, maybeConfig, maybeConfigParsingError)
85+
cache = RepoCache(sha1, commitDate, dependencyInfos, maybeConfig, maybeConfigParsingError)
8186
} yield RepoData(repo, cache, config)
8287

8388
private def gatherDependencyInfo(repo: Repo, dependency: Dependency): F[DependencyInfo] =
8489
gitAlg.findFilesContaining(repo, dependency.version.value).map(DependencyInfo(dependency, _))
90+
91+
private[repocache] def throwIfAbandoned(data: RepoData): F[Unit] =
92+
data.config.lastCommitMaxAge.traverse_ { maxAge =>
93+
dateTimeAlg.currentTimestamp.flatMap { now =>
94+
val sinceLastCommit = data.cache.commitDate.until(now)
95+
val isAbandoned = sinceLastCommit > maxAge
96+
F.raiseWhen(isAbandoned) {
97+
val msg = s"Skipping because last commit is older than ${dateTime.showDuration(maxAge)}"
98+
new Throwable(msg) with NoStackTrace
99+
}
100+
}
101+
}
85102
}

modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfig.scala

+9-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import org.scalasteward.core.buildtool.BuildRoot
2525
import org.scalasteward.core.data.Repo
2626
import org.scalasteward.core.edit.hooks.PostUpdateHook
2727
import org.scalasteward.core.repoconfig.RepoConfig.defaultBuildRoots
28+
import org.scalasteward.core.util.dateTime.*
29+
import org.scalasteward.core.util.{combineOptions, intellijThisImportIsUsed}
30+
import scala.concurrent.duration.FiniteDuration
2831

2932
final case class RepoConfig(
3033
private val commits: Option[CommitsConfig] = None,
@@ -37,7 +40,8 @@ final case class RepoConfig(
3740
private val assignees: Option[List[String]] = None,
3841
private val reviewers: Option[List[String]] = None,
3942
private val dependencyOverrides: Option[List[GroupRepoConfig]] = None,
40-
signoffCommits: Option[Boolean] = None
43+
signoffCommits: Option[Boolean] = None,
44+
lastCommitMaxAge: Option[FiniteDuration] = None
4145
) {
4246
def commitsOrDefault: CommitsConfig =
4347
commits.getOrElse(CommitsConfig())
@@ -107,8 +111,11 @@ object RepoConfig {
107111
assignees = x.assignees |+| y.assignees,
108112
reviewers = x.reviewers |+| y.reviewers,
109113
dependencyOverrides = x.dependencyOverrides |+| y.dependencyOverrides,
110-
signoffCommits = x.signoffCommits.orElse(y.signoffCommits)
114+
signoffCommits = x.signoffCommits.orElse(y.signoffCommits),
115+
lastCommitMaxAge = combineOptions(x.lastCommitMaxAge, y.lastCommitMaxAge)(_ max _)
111116
)
112117
}
113118
)
119+
120+
intellijThisImportIsUsed(finiteDurationEncoder)
114121
}

modules/core/src/main/scala/org/scalasteward/core/util/Timestamp.scala

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ final case class Timestamp(millis: Long) {
3434
}
3535

3636
object Timestamp {
37+
def fromEpochSecond(seconds: Long): Timestamp =
38+
Timestamp(seconds * 1000L)
39+
3740
def fromLocalDateTime(ldt: LocalDateTime): Timestamp =
3841
Timestamp(ldt.toInstant(ZoneOffset.UTC).toEpochMilli)
3942

modules/core/src/main/scala/org/scalasteward/core/util/dateTime.scala

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.scalasteward.core.util
1818

1919
import cats.syntax.all.*
20+
import io.circe.{Decoder, Encoder}
2021
import java.util.concurrent.TimeUnit
2122
import scala.annotation.tailrec
2223
import scala.concurrent.duration.*
@@ -31,6 +32,12 @@ object dateTime {
3132
def renderFiniteDuration(fd: FiniteDuration): String =
3233
fd.toString.filterNot(_.isSpaceChar)
3334

35+
implicit val finiteDurationDecoder: Decoder[FiniteDuration] =
36+
Decoder[String].emap(parseFiniteDuration(_).leftMap(_.getMessage))
37+
38+
implicit val finiteDurationEncoder: Encoder[FiniteDuration] =
39+
Encoder[String].contramap(renderFiniteDuration)
40+
3441
def showDuration(d: FiniteDuration): String = {
3542
def symbol(unit: TimeUnit): String =
3643
unit match {

modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.scalasteward.core.git.Sha1
1010
import org.scalasteward.core.repocache.RepoCache
1111
import org.scalasteward.core.repoconfig.*
1212
import org.scalasteward.core.repoconfig.PullRequestFrequency.{Asap, Timespan}
13+
import org.scalasteward.core.util.{DateTimeAlg, Timestamp}
1314
import org.typelevel.log4cats.Logger
1415
import org.typelevel.log4cats.slf4j.Slf4jLogger
1516
import scala.concurrent.duration.FiniteDuration
@@ -19,11 +20,14 @@ object TestInstances {
1920
Sha1.unsafeFrom("da39a3ee5e6b4b0d3255bfef95601890afd80709")
2021

2122
val dummyRepoCache: RepoCache =
22-
RepoCache(dummySha1, List.empty, Option.empty, Option.empty)
23+
RepoCache(dummySha1, Timestamp(0L), List.empty, Option.empty, Option.empty)
2324

2425
val dummyRepoCacheWithParsingError: RepoCache =
2526
dummyRepoCache.copy(maybeRepoConfigParsingError = Some("Failed to parse .scala-steward.conf"))
2627

28+
val ioDateTimeAlg: DateTimeAlg[IO] =
29+
DateTimeAlg.create[IO]
30+
2731
implicit val ioLogger: Logger[IO] =
2832
Slf4jLogger.getLogger[IO]
2933

modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import cats.Monad
55
import cats.effect.IO
66
import cats.syntax.all.*
77
import munit.CatsEffectSuite
8-
import org.scalasteward.core.TestInstances.ioLogger
8+
import org.scalasteward.core.TestInstances.{ioDateTimeAlg, ioLogger}
99
import org.scalasteward.core.git.FileGitAlgTest.{
1010
conflictsNo,
1111
conflictsYes,
@@ -18,6 +18,7 @@ import org.scalasteward.core.io.ProcessAlgTest.ioProcessAlg
1818
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
1919
import org.scalasteward.core.mock.MockConfig.{config, mockRoot}
2020
import org.scalasteward.core.util.Nel
21+
import scala.concurrent.duration.DurationInt
2122

2223
class FileGitAlgTest extends CatsEffectSuite {
2324
private val rootDir = mockRoot / "git-tests"
@@ -158,6 +159,19 @@ class FileGitAlgTest extends CatsEffectSuite {
158159
} yield ()
159160
}
160161

162+
test("getCommitDate") {
163+
val repo = rootDir / "getCommitDate"
164+
for {
165+
_ <- ioAuxGitAlg.createRepo(repo)
166+
sha1 <- ioGitAlg.latestSha1(repo, master)
167+
commitDate <- ioGitAlg.getCommitDate(repo, sha1)
168+
now <- ioDateTimeAlg.currentTimestamp
169+
diff = commitDate.until(now)
170+
maxDrift = 2.hours
171+
_ = assert(diff > -maxDrift && diff < maxDrift, clue((commitDate, now)))
172+
} yield ()
173+
}
174+
161175
test("hasConflicts") {
162176
val repo = rootDir / "hasConflicts"
163177
for {

modules/core/src/test/scala/org/scalasteward/core/io/processTest.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package org.scalasteward.core.io
33
import cats.effect.IO
44
import cats.effect.unsafe.implicits.global
55
import munit.FunSuite
6+
import org.scalasteward.core.TestInstances.ioDateTimeAlg
67
import org.scalasteward.core.io.process.*
7-
import org.scalasteward.core.util.{DateTimeAlg, Nel}
8+
import org.scalasteward.core.util.Nel
89
import scala.concurrent.duration.*
910

1011
class processTest extends FunSuite {
@@ -66,7 +67,7 @@ class processTest extends FunSuite {
6667
val timeout = 500.milliseconds
6768
val sleep = timeout * 2
6869
val p = slurp2(Nel.of("sleep", sleep.toSeconds.toInt.toString), timeout).attempt
69-
val (Left(t), fd) = DateTimeAlg.create[IO].timed(p).unsafeRunSync(): @unchecked
70+
val (Left(t), fd) = ioDateTimeAlg.timed(p).unsafeRunSync(): @unchecked
7071

7172
assert(clue(t).isInstanceOf[ProcessTimedOutException])
7273
assert(clue(fd) > timeout)

modules/core/src/test/scala/org/scalasteward/core/repocache/RepoCacheAlgTest.scala

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package org.scalasteward.core.repocache
22

3-
import cats.implicits.toSemigroupKOps
3+
import cats.syntax.all.*
44
import io.circe.syntax.*
5+
import java.time.LocalDateTime
56
import munit.CatsEffectSuite
67
import org.http4s.HttpApp
78
import org.http4s.circe.*
@@ -14,7 +15,9 @@ import org.scalasteward.core.forge.github.Repository
1415
import org.scalasteward.core.git.Branch
1516
import org.scalasteward.core.mock.MockContext.context.{repoCacheAlg, repoConfigAlg, workspaceAlg}
1617
import org.scalasteward.core.mock.{GitHubAuth, MockEff, MockEffOps, MockState}
17-
import org.scalasteward.core.util.intellijThisImportIsUsed
18+
import org.scalasteward.core.repoconfig.RepoConfig
19+
import org.scalasteward.core.util.{intellijThisImportIsUsed, Timestamp}
20+
import scala.concurrent.duration.*
1821

1922
class RepoCacheAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
2023
intellijThisImportIsUsed(encodeUri)
@@ -36,7 +39,8 @@ class RepoCacheAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
3639
uri"https://github.com/scala-steward/cats-effect.git",
3740
Branch("main")
3841
)
39-
val repoCache = RepoCache(dummySha1, Nil, None, None)
42+
val now = Timestamp.fromLocalDateTime(LocalDateTime.now())
43+
val repoCache = RepoCache(dummySha1, now, Nil, None, None)
4044
val workspace = workspaceAlg.rootDir.unsafeRunSync()
4145
val httpApp = HttpApp[MockEff] {
4246
case POST -> Root / "repos" / "typelevel" / "cats-effect" / "forks" =>
@@ -55,4 +59,33 @@ class RepoCacheAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
5559
val expected = (RepoData(repo, repoCache, repoConfigAlg.mergeWithGlobal(None)), repoOut)
5660
assertIO(obtained, expected)
5761
}
62+
63+
test("throwIfAbandoned: no maxAge") {
64+
val repo = Repo("repo-cache-alg", "test-1")
65+
val cache = RepoCache(dummySha1, Timestamp(0L), Nil, None, None)
66+
val config = RepoConfig.empty
67+
val data = RepoData(repo, cache, config)
68+
val obtained = repoCacheAlg.throwIfAbandoned(data).runA(MockState.empty).attempt
69+
assertIO(obtained, Right(()))
70+
}
71+
72+
test("throwIfAbandoned: lastCommit < maxAge") {
73+
val repo = Repo("repo-cache-alg", "test-2")
74+
val commitDate = Timestamp.fromLocalDateTime(LocalDateTime.now())
75+
val cache = RepoCache(dummySha1, commitDate, Nil, None, None)
76+
val config = RepoConfig(lastCommitMaxAge = Some(1.day))
77+
val data = RepoData(repo, cache, config)
78+
val obtained = repoCacheAlg.throwIfAbandoned(data).runA(MockState.empty).attempt
79+
assertIO(obtained, Right(()))
80+
}
81+
82+
test("throwIfAbandoned: lastCommit > maxAge") {
83+
val repo = Repo("repo-cache-alg", "test-3")
84+
val cache = RepoCache(dummySha1, Timestamp(0L), Nil, None, None)
85+
val config = RepoConfig(lastCommitMaxAge = Some(1.day))
86+
val data = RepoData(repo, cache, config)
87+
val obtained =
88+
repoCacheAlg.throwIfAbandoned(data).runA(MockState.empty).attempt.map(_.leftMap(_.getMessage))
89+
assertIO(obtained, Left("Skipping because last commit is older than 1d"))
90+
}
5891
}

modules/core/src/test/scala/org/scalasteward/core/update/PruningAlgTest.scala

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class PruningAlgTest extends FunSuite {
2121
val Right(repoCache) = decode[RepoCache](
2222
s"""|{
2323
| "sha1": "12def27a837ba6dc9e17406cbbe342fba3527c14",
24+
| "commitDate": 0,
2425
| "dependencyInfos": [],
2526
| "maybeRepoConfig": {
2627
| "pullRequests": {
@@ -79,6 +80,7 @@ class PruningAlgTest extends FunSuite {
7980
val Right(repoCache) = decode[RepoCache](
8081
s"""|{
8182
| "sha1": "12def27a837ba6dc9e17406cbbe342fba3527c14",
83+
| "commitDate": 0,
8284
| "dependencyInfos" : [
8385
| {
8486
| "value" : [
@@ -218,6 +220,7 @@ class PruningAlgTest extends FunSuite {
218220
val Right(repoCache) = decode[RepoCache](
219221
s"""|{
220222
| "sha1": "12def27a837ba6dc9e17406cbbe342fba3527c14",
223+
| "commitDate": 0,
221224
| "dependencyInfos" : [
222225
| {
223226
| "value" : [
@@ -330,6 +333,7 @@ class PruningAlgTest extends FunSuite {
330333
val Right(repoCache) = decode[RepoCache](
331334
s"""|{
332335
| "sha1": "12def27a837ba6dc9e17406cbbe342fba3527c14",
336+
| "commitDate": 0,
333337
| "dependencyInfos" : [
334338
| {
335339
| "value" : [
@@ -441,6 +445,7 @@ class PruningAlgTest extends FunSuite {
441445
val Right(repoCache) = decode[RepoCache](
442446
s"""|{
443447
| "sha1": "12def27a837ba6dc9e17406cbbe342fba3527c14",
448+
| "commitDate": 0,
444449
| "dependencyInfos" : [
445450
| {
446451
| "value" : [

0 commit comments

Comments
 (0)