Skip to content

Commit

Permalink
Refactor cmta new
Browse files Browse the repository at this point in the history
  • Loading branch information
eloots committed Jul 13, 2023
1 parent f2ee46e commit 4a798a1
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 54 deletions.
33 changes: 28 additions & 5 deletions cmta/src/main/scala/com/lunatech/cmt/admin/Domain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ package com.lunatech.cmt.admin
* See the License for the specific language governing permissions and limitations under the License.
*/

import com.lunatech.cmt.Domain.InstallationSource.GithubProject
import com.lunatech.cmt.CmtError
import sbt.io.syntax.File
import com.lunatech.cmt.*
import cats.syntax.either.*

object Domain:

Expand All @@ -39,12 +41,33 @@ object Domain:
final case class MainRepository(value: File)
final case class ConfigurationFile(value: File)

final case class CourseTemplate(value: GithubProject)
final case class CourseTemplate(value: Either[CmtError, InstallationSource.GithubProject])
object CourseTemplate:
val GithubProjectRegex = "([A-Za-z0-9-_]*)\\/([A-Za-z0-9-_]*)".r
val GithubProjectWithTagRegex = "([A-Za-z0-9-_]*)\\/([A-Za-z0-9-_]*)\\/(.*)".r
def fromString(str: String): CourseTemplate =
if (str.indexOf("/") > 0) {
val Array(organisation, project) = str.split("/").map(_.trim)
CourseTemplate(GithubProject(organisation, project))
str match {
case GithubProjectRegex(organisation, project) =>
CourseTemplate(Right(InstallationSource.GithubProject(organisation, project, None)))
case GithubProjectWithTagRegex(organisation, project, tag) =>
CourseTemplate(Right(InstallationSource.GithubProject(organisation, project, Some(tag))))
case _ => CourseTemplate(s"Invalid template name: $str".toExecuteCommandErrorMessage.asLeft)
}
} else {
CourseTemplate(GithubProject("lunatech-labs", str))
CourseTemplate(Right(InstallationSource.GithubProject("lunatech-labs", str, None)))
}

sealed trait InstallationSource

object InstallationSource:
final case class LocalDirectory(value: File) extends InstallationSource

final case class ZipFile(value: File) extends InstallationSource

final case class GithubProject(organisation: String, project: String, tag: Option[String])
extends InstallationSource {
val displayName: String =
if tag.isEmpty then s"$organisation/$project" else s"$organisation/$project/${tag.getOrElse("")}"
}
end InstallationSource
106 changes: 89 additions & 17 deletions cmta/src/main/scala/com/lunatech/cmt/admin/command/New.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@ package com.lunatech.cmt.admin.command
import caseapp.{AppName, CommandName, ExtraName, HelpMessage, RemainingArgs, ValueDescription}
import com.lunatech.cmt.{CmtError, printResult}
import com.lunatech.cmt.admin.Domain.{ConfigurationFile, CourseTemplate}
import com.lunatech.cmt.client.command.{Executable, Install}
import com.lunatech.cmt.client.command.Executable
import com.lunatech.cmt.core.validation.Validatable
import com.lunatech.cmt.admin.cli.ArgParsers.{configurationFileArgParser, courseTemplateArgParser}
import com.lunatech.cmt.client.Configuration
import com.lunatech.cmt.client.cli.CmtcCommand
import com.lunatech.cmt.admin.Domain.InstallationSource.*
import com.lunatech.cmt.Helpers.ignoreProcessStdOutStdErr
import sbt.io.IO as sbtio
import sbt.io.syntax.*
import com.lunatech.cmt.*

import cats.syntax.either.*

import java.io.FileFilter
import sys.process.*
import util.{Try, Success, Failure}

object New:

private final case class TagSet(tags: Vector[String])

@AppName("new")
@CommandName("new")
@HelpMessage(
Expand All @@ -38,22 +47,85 @@ object New:

given Executable[New.Options] with
extension (options: New.Options)
def execute(configuration: Configuration): Either[CmtError, String] = {
// list the contents of the ~/Courses directory, if there's anything already matching the name then get the count so we can append to the name and prevent conflicts
val existingFilesWithSameName = sbtio.listFiles(
configuration.coursesDirectory.value,
new FileFilter {
override def accept(file: File): Boolean =
file.name.startsWith(options.template.value.project)
})
val discriminator = if (existingFilesWithSameName.size > 0) s"-${existingFilesWithSameName.size}" else ""
val targetDirectoryName = s"${options.template.value.project}$discriminator"

// Install the course
Install
.Options(options.template.value, newName = Some(targetDirectoryName), studentifiedAsset = Some(false))
.execute(configuration)
}
def execute(configuration: Configuration): Either[CmtError, String] =
for {
// list the contents of the ~/Courses directory, if there's anything already matching the
// name then get the count so we can append to the name and prevent conflicts
template <- options.template.value
existingFilesWithSameName = sbtio.listFiles(
configuration.coursesDirectory.value,
new FileFilter {
override def accept(file: File): Boolean =
file.name.startsWith(template.project)
})
discriminator = if (existingFilesWithSameName.size > 0) s"-${existingFilesWithSameName.size}" else ""
targetDirectoryName = s"${template.project}$discriminator"
newRepo <- newCmtRepoFromGithubProject(template, targetDirectoryName, configuration)
} yield newRepo

private def cloneMainRepo(githubProject: GithubProject, tmpDir: File): Either[CmtError, TagSet] =
val project = githubProject.project
val organisation = githubProject.organisation
val tag = githubProject.tag
val cloneGit = Process(Seq("git", "clone", s"[email protected]:$organisation/$project.git"), tmpDir)
val cloneHttp = Process(Seq("git", "clone", s"[email protected]:$organisation/$project.git"), tmpDir)
val cloneGh = Process(Seq("gh", "repo", "clone", s"$organisation/$project"), tmpDir)
val cloneRepoStatus =
Try(cloneGit.!).recoverWith(_ => Try(cloneHttp.!)).recoverWith(_ => Try(cloneGh.!)) match {
case Success(x) =>
if x == 0 then Right(x)
else s"Cannot install from ${githubProject.displayName}: No such repo".toExecuteCommandErrorMessage.asLeft
case Failure(_) => s"Cannot install from ${githubProject.displayName}: No such repo".toExecuteCommandErrorMessage.asLeft
}
for {
_ <- cloneRepoStatus
tags = Process(Seq("git", "tag", "--sort", "v:refname"), tmpDir / project).!!.split("\n").to(Vector)
} yield TagSet(tags)

private def newCmtRepoFromGithubProject(
githubProject: GithubProject,
targetDirectoryName: String,
configuration: Configuration): Either[CmtError, String] =
val tmpDir = sbtio.createTemporaryDirectory
val installResult = for {
tagSet <- cloneMainRepo(githubProject, tmpDir)
result <- downloadAndInstallRepo(githubProject, targetDirectoryName, configuration, tagSet, tmpDir)
} yield result
sbtio.delete(tmpDir)
installResult

private def copyRepo(
githubProject: GithubProject,
targetDirectoryName: String,
configuration: Configuration,
tag: String,
tmpDir: File): Unit =
Process(Seq("git", "checkout", tag), tmpDir / githubProject.project).!(ignoreProcessStdOutStdErr)
sbtio.copyDirectory(tmpDir / githubProject.project, configuration.coursesDirectory.value / targetDirectoryName)
sbtio.delete(configuration.coursesDirectory.value / targetDirectoryName / ".git")
Helpers.initializeGitRepo(configuration.coursesDirectory.value / targetDirectoryName)
val _ = Helpers.commitToGit("Initial commit", configuration.coursesDirectory.value / targetDirectoryName)

private def downloadAndInstallRepo(
githubProject: GithubProject,
targetDirectoryName: String,
configuration: Configuration,
tagSet: TagSet,
tmpDir: File): Either[CmtError, String] =
(githubProject.tag, tagSet.tags.isEmpty, tagSet.tags.lastOption) match {
case (None, _, Some(lastReleaseTag)) =>
copyRepo(githubProject, targetDirectoryName, configuration, lastReleaseTag, tmpDir)
Right(
s"${githubProject.copy(tag = Some(lastReleaseTag)).displayName} successfully installed to ${configuration.coursesDirectory.value}/${targetDirectoryName}")
case (Some(tag), false, _) if tagSet.tags.contains(tag) =>
copyRepo(githubProject, targetDirectoryName, configuration, tag, tmpDir)
Right(
s"${githubProject.displayName} successfully installed to ${configuration.coursesDirectory.value}/${targetDirectoryName}")
case (Some(tag), false, _) =>
s"Cannot install from ${githubProject.displayName}. No such tag: $tag".toExecuteCommandErrorMessage.asLeft
case (Some(_), true, _) | (None, _, None) =>
s"Cannot install from ${githubProject.displayName}: No releases found".toExecuteCommandErrorMessage.asLeft
}

val command = new CmtcCommand[New.Options] {
def run(options: New.Options, args: RemainingArgs): Unit =
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala/com/lunatech/cmt/Domain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ object Domain {
object InstallationSource:
final case class LocalDirectory(value: File) extends InstallationSource
final case class ZipFile(value: File) extends InstallationSource
final case class GithubProject(organisation: String, project: String) extends InstallationSource {
val displayName = s"$organisation/$project"
final case class GithubProject(organisation: String, project: String, tag: Option[String])
extends InstallationSource {
val displayName: String =
if tag.isEmpty then s"$organisation/$project" else s"$organisation/$project/${tag.getOrElse("")}"
}
end InstallationSource
}
2 changes: 2 additions & 0 deletions core/src/main/scala/com/lunatech/cmt/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ object Helpers:
d.toInt
}

val ignoreProcessStdOutStdErr: sys.process.ProcessLogger =
sys.process.ProcessLogger(_ => (), _ => ())
def copyCleanViaGit(mainRepo: File, tmpDir: File, repoName: String): Either[CmtError, Unit] =

import ProcessDSL.*
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/com/lunatech/cmt/ProcessDSL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package com.lunatech.cmt

import com.lunatech.cmt.toExecuteCommandErrorMessage
import sbt.io.syntax.*
import com.lunatech.cmt.Helpers.ignoreProcessStdOutStdErr

import scala.sys.process.Process
import scala.util.{Failure, Success, Try}
Expand All @@ -26,7 +27,7 @@ object ProcessDSL:
extension (cmd: ProcessCmd)

def runWithStatus(msg: String): Either[CmtError, Unit] = {
val status = Try(Process(cmd.cmd, cmd.workingDir).!)
val status = Try(Process(cmd.cmd, cmd.workingDir).!(ignoreProcessStdOutStdErr))
status match
case Success(_) => Right(())
case Failure(ex) => Left(msg.toExecuteCommandErrorMessage)
Expand Down
30 changes: 6 additions & 24 deletions core/src/main/scala/com/lunatech/cmt/client/command/Install.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ object Install:
"Install a course - from either a local directory, a zip file on the local file system or a Github project")
final case class Options(
@ExtraName("s")
source: InstallationSource,
@ExtraName("d")
newName: Option[String] = None,
@ExtraName("a")
studentifiedAsset: Option[Boolean] = Some(true))
source: InstallationSource)

given Validatable[Install.Options] with
extension (options: Install.Options)
Expand Down Expand Up @@ -97,13 +93,11 @@ object Install:
release: Release,
configuration: Configuration)(implicit client: Client[IO]): Either[CmtError, String] =
for {
assetUrl <- getAssetUrl(githubProject, release)
_ = printMessage(s"downloading studentified course from '$assetUrl' to courses directory")
downloadedZipFile <- downloadStudentAsset(assetUrl, githubProject, release.tag_name, configuration)
studentAssetUrl <- getStudentAssetUrl(githubProject, release)
_ = printMessage(s"downloading studentified course from '$studentAssetUrl' to courses directory")
downloadedZipFile <- downloadStudentAsset(studentAssetUrl, githubProject, release.tag_name, configuration)
_ <- installFromZipFile(downloadedZipFile, configuration, deleteZipAfterInstall = true)
_ <-
if (cmd.studentifiedAsset.getOrElse(true)) setCurrentCourse(githubProject, configuration)
else "successfully created new course".toExecuteCommandErrorMessage.asRight
_ <- setCurrentCourse(githubProject, configuration)
} yield s"${githubProject.project} (${release.tag_name}) successfully installed to ${configuration.coursesDirectory.value}/${githubProject.project}"

private def setCurrentCourse(
Expand All @@ -114,24 +108,12 @@ object Install:
SetCurrentCourse.Options(studentifiedRepo).execute(configuration)
}

private def getAssetUrl(githubProject: GithubProject, release: Release)(implicit
httpClient: Client[IO]): Either[CmtError, String] = {
if (cmd.studentifiedAsset.getOrElse(true)) {
getStudentAssetUrl(githubProject, release)
} else {
release.zipball_url.toRight(
s"latest release of ${githubProject.displayName} does not have a 'main' repo zip - unable to install without one".toExecuteCommandErrorMessage)
}
}

private def getStudentAssetUrl(githubProject: GithubProject, release: Release)(implicit
httpClient: Client[IO]): Either[CmtError, String] = {
val maybeAssetsFuture = httpClient
.expect[List[Asset]](release.assets_url)
.map { assets =>
val requiredName =
if (cmd.studentifiedAsset.getOrElse(true)) s"${githubProject.project}-student.zip"
else s"${githubProject.project}-${release.tag_name}.zip"
val requiredName = s"${githubProject.project}-student.zip"
assets.find(_.name == requiredName).map(_.browserDownloadUrl)
}
.unsafeToFuture()
Expand Down
14 changes: 9 additions & 5 deletions core/src/main/scala/com/lunatech/cmt/core/cli/ArgParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,23 @@ object ArgParsers:
given installationSourceArgParser: ArgParser[InstallationSource] = {

val githubProjectRegex = "([A-Za-z0-9-_]*)\\/([A-Za-z0-9-_]*)".r
val githubProjectWithTagRegex = "([A-Za-z0-9-_]*)\\/([A-Za-z0-9-_]*)\\/(.*)".r

def toString(installationSource: InstallationSource): String =
installationSource match {
case LocalDirectory(value) => value.getAbsolutePath()
case ZipFile(value) => value.getAbsolutePath()
case GithubProject(organisation, project) => s"$organisation/$project"
case LocalDirectory(value) => value.getAbsolutePath()
case ZipFile(value) => value.getAbsolutePath()
case GithubProject(organisation, project, None) => s"$organisation/$project"
case GithubProject(organisation, project, Some(tag)) => s"$organisation/$project/$tag"
}

def fromString(str: String): Either[Error, InstallationSource] = {
val maybeFile = file(str)
val maybeGithub = str match {
case githubProjectRegex(organisation, project) => Some(GithubProject(organisation, project))
case _ => None
case githubProjectRegex(organisation, project) => Some(GithubProject(organisation, project, None))
case githubProjectWithTagRegex(organisation, project, tag) =>
Some(GithubProject(organisation, project, Some(tag)))
case _ => None
}

// is it a file? does it exist?
Expand Down

0 comments on commit 4a798a1

Please sign in to comment.