Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #218 Support of git tags #267

Merged
merged 2 commits into from
Mar 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/src/main/scala/giter8.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ class Giter8 extends xsbti.AppMain {
config.copy(repo = repo)
} text ("git or file URL, or github user/repo")
opt[String]('b', "branch") action { (b, config) =>
config.copy(branch = Some(b))
config.copy(ref = Some(Branch(b)))
} text("Resolve a template within a given branch")
opt[String]('t', "tag") action { (t, config) =>
config.copy(ref = Some(Tag(t)))
} text("Resolve a template within a given branch")
opt[String]('d', "directory") action { (d, config) =>
config.copy(directory = Some(d))
Expand All @@ -79,6 +82,9 @@ class Giter8 extends xsbti.AppMain {
|Apply template from a remote branch
| g8 foundweekends/giter8 -b some-branch
|
|Apply template from a remote tag
| g8 foundweekends/giter8 -t some-tag
|
|Apply template from a directory that exists in the repo
| g8 foundweekends/giter8 -d some-directory/template
|
Expand Down
24 changes: 16 additions & 8 deletions library/src/main/scala/giter8/Git.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,38 @@ import scala.collection.JavaConverters._
class Git(gitInteractor: GitInteractor) {
import Git._

def clone(repository: GitRepository, branch: Option[String], destination: File): Try[Unit] = repository match {
case remote: Remote => cloneWithGivenOrDefaultBranch(remote.url, branch, destination)
case local: Local => branch match {
def clone(repository: GitRepository, ref: Option[Ref], destination: File): Try[Unit] = repository match {
case remote: Remote => cloneWithGivenRefOrDefaultBranch(remote.url, ref, destination)
case local: Local => ref match {
// for file:// repositories with no named branch, just do a file copy (assume current branch)
case None => copy(new File(local.path), destination)
case Some(_) => cloneWithGivenOrDefaultBranch(local.path, branch, destination)
case Some(_) => cloneWithGivenRefOrDefaultBranch(local.path, ref, destination)
}
case github: GitHub => cloneWithGivenOrDefaultBranch(github.publicUrl, branch, destination) recoverWith {
case github: GitHub => cloneWithGivenRefOrDefaultBranch(github.publicUrl, ref, destination) recoverWith {
case _: TransportError =>
cleanDir(destination)
cloneWithGivenOrDefaultBranch(github.privateUrl, branch, destination)
cloneWithGivenRefOrDefaultBranch(github.privateUrl, ref, destination)
}
}

private def cloneWithGivenOrDefaultBranch(url: String, branch: Option[String], dest: File): Try[Unit] = branch match {
private def cloneWithGivenRefOrDefaultBranch(url: String, ref: Option[Ref], dest: File): Try[Unit] = ref match {
case None => gitInteractor.cloneRepository(url, dest) flatMap { _ =>
gitInteractor.getDefaultBranch(dest) flatMap { branch =>
gitInteractor.checkoutBranch(dest, branch)
}
}
case Some(br) => gitInteractor.getRemoteBranches(url) flatMap { remoteBranches =>
case Some(Branch(br)) => gitInteractor.getRemoteBranches(url) flatMap { remoteBranches =>
if (!remoteBranches.contains(br)) Failure(NoBranchError(br))
else gitInteractor.cloneRepository(url, dest) flatMap { _ =>
gitInteractor.checkoutBranch(dest, br)
}
}
case Some(Tag(t)) => gitInteractor.getRemoteTags(url) flatMap { remoteTags =>
if (!remoteTags.contains(t)) Failure(NoTagError(t))
else gitInteractor.cloneRepository(url, dest) flatMap { _ =>
gitInteractor.checkoutTag(dest, t)
}
}
}

// Protected for testing: see GitTest.scala
Expand All @@ -83,4 +89,6 @@ class Git(gitInteractor: GitInteractor) {
object Git {
case class CloneError(message: String) extends RuntimeException(message)
case class NoBranchError(branchName: String) extends RuntimeException(s"No branch $branchName")
case class NoTagError(tagName: String) extends RuntimeException(s"No tag $tagName")
}

15 changes: 15 additions & 0 deletions library/src/main/scala/giter8/GitInteractor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import scala.collection.JavaConverters._
trait GitInteractor {
def cloneRepository(url: String, dest: File): Try[Unit]
def getRemoteBranches(url: String): Try[Seq[String]]
def getRemoteTags(url: String): Try[Seq[String]]
def getDefaultBranch(repository: File): Try[String]
def checkoutBranch(repository: File, branch: String): Try[Unit]
def checkoutTag(repository: File, tag: String): Try[Unit]
}

object GitInteractor {
Expand All @@ -58,6 +60,14 @@ class JGitInteractor extends GitInteractor {
}
}

override def getRemoteTags(url: String): Try[Seq[String]] = {
Try(JGit.lsRemoteRepository().setRemote(url).setHeads(false).setTags(true).call()) map { refs =>
refs.asScala.map(r => r.getName.stripPrefix("refs/tags/")).toSeq
} recoverWith {
case e: TransportException => Failure(TransportError(e.getMessage))
}
}

override def getDefaultBranch(repository: File): Try[String] = Try {
val git = JGit.open(repository)
try {
Expand All @@ -82,4 +92,9 @@ class JGitInteractor extends GitInteractor {
}
}
}

override def checkoutTag(repository: File, tag: String): Try[Unit] = Try {
val git = JGit.open(repository)
Try(git.checkout().setName(tag).call()).map(_ => git.close())
}
}
14 changes: 10 additions & 4 deletions library/src/main/scala/giter8/JgitHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import org.apache.commons.io.FileUtils

import scala.util.{Failure, Success}

case class Config(repo: String, branch: Option[String] = None, forceOverwrite: Boolean = false, directory: Option[String] = None)
sealed trait Ref

case class Tag(name: String) extends Ref

case class Branch(name: String) extends Ref

case class Config(repo: String, ref: Option[Ref] = None, forceOverwrite: Boolean = false, directory: Option[String] = None)

class JgitHelper(gitInteractor: Git, templateRenderer: TemplateRenderer) {

Expand All @@ -47,11 +53,11 @@ class JgitHelper(gitInteractor: Git, templateRenderer: TemplateRenderer) {

def run(config: Config, arguments: Seq[String], outDirectory: File): Either[String, String] = for {
repository <- GitRepository.fromString(config.repo)
baseDir <- gitInteractor.clone(repository, config.branch, tempdir) match {
baseDir <- gitInteractor.clone(repository, config.ref, tempdir) match {
case Success(_) => Right(new File(tempdir, config.directory.getOrElse("")))
case Failure(e) => Left(e.getMessage)
}
foo <- templateRenderer.render(baseDir, outDirectory, arguments, config.forceOverwrite)
} yield foo
renderedTemplate <- templateRenderer.render(baseDir, outDirectory, arguments, config.forceOverwrite)
} yield renderedTemplate

}
33 changes: 27 additions & 6 deletions library/src/test/scala/giter8/GitTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package giter8

import java.io.File

import giter8.Git.NoBranchError
import giter8.Git.{NoBranchError, NoTagError}
import giter8.GitInteractor.TransportError
import giter8.GitRepository.{GitHub, Local, Remote}
import org.scalatest.{EitherValues, FlatSpec, Matchers, TryValues}
Expand All @@ -27,10 +27,10 @@ class GitTest extends FlatSpec with Matchers with EitherValues with TryValues wi

"Git" should "clone repository with given branch" in new TestFixture {
val repository = Remote("url")
val branch = Some("fooBranch")
val branch = Some(Branch("fooBranch"))
val destination = new File(".")

interactorMock.getRemoteBranches _ expects repository.url returning Success(Seq(branch.get))
interactorMock.getRemoteBranches _ expects repository.url returning Success(Seq(branch.get.name))
interactorMock.cloneRepository _ expects (repository.url, destination) returning Success(())
interactorMock.checkoutBranch _ expects (destination, "fooBranch") returning Success(())

Expand All @@ -39,13 +39,34 @@ class GitTest extends FlatSpec with Matchers with EitherValues with TryValues wi

it should "throw an error if there is no given branch" in new TestFixture {
val repository = Remote("url")
val branch = Some("nonExisting")
val branch = Some(Branch("nonExisting"))
val destination = new File(".")

interactorMock.getRemoteBranches _ expects repository.url returning Success(Seq("someOtherBranch"))
git.clone(repository, branch, destination).failure.exception.getClass shouldBe classOf[NoBranchError]
}

it should "clone repository with given tag" in new TestFixture {
val repository = Remote("url")
val tag = Some(Tag("v1.0.0"))
val destination = new File(".")

interactorMock.getRemoteTags _ expects repository.url returning Success(Seq(tag.get.name))
interactorMock.cloneRepository _ expects (repository.url, destination) returning Success(())
interactorMock.checkoutTag _ expects (destination, "v1.0.0") returning Success(())

git.clone(repository, tag, destination)
}

it should "throw an error if there is no given tag" in new TestFixture {
val repository = Remote("url")
val tag = Some(Tag("nonExisting"))
val destination = new File(".")

interactorMock.getRemoteTags _ expects repository.url returning Success(Seq("someOtherBranch"))
git.clone(repository, tag, destination).failure.exception.getClass shouldBe classOf[NoTagError]
}

it should "clone repository with default branch if no branch was given" in new TestFixture {
val repository = Remote("url")
val destination = new File(".")
Expand All @@ -68,11 +89,11 @@ class GitTest extends FlatSpec with Matchers with EitherValues with TryValues wi

it should "retry cloning Github repository with given branch if clone with public URL is failed" in new TestFixture {
val repository = GitHub("foo", "bar")
val branch = Some("someBranch")
val branch = Some(Branch("someBranch"))
val destination = new File(".")

interactorMock.getRemoteBranches _ expects repository.publicUrl returning Failure(TransportError(""))
interactorMock.getRemoteBranches _ expects repository.privateUrl returning Success(Seq(branch.get))
interactorMock.getRemoteBranches _ expects repository.privateUrl returning Success(Seq(branch.get.name))
interactorMock.cloneRepository _ expects (repository.privateUrl, destination) returning Success(())
interactorMock.checkoutBranch _ expects (destination, "someBranch") returning Success(())

Expand Down
28 changes: 28 additions & 0 deletions library/src/test/scala/giter8/JGitInteractorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ class JGitInteractorTest extends FlatSpec with Matchers with BeforeAndAfter with
interactor.getRemoteBranches(remoteRepository.getAbsolutePath).success.value should contain theSameElementsAs branches :+ "master"
}

it should "retrieve tag list from remote repository" in {
val tags = Seq("v1.0.0", "some_tag")

tags foreach { tag =>
"foo" >> (remoteRepository / tag)
remoteRepository.commit(s"New tag $tag")
remoteRepository.tag(tag)
}

interactor.getRemoteTags(remoteRepository.getAbsolutePath).success.value should contain theSameElementsAs tags
}

it should "checkout repository to given branch" in tempDirectory { localRepository =>
remoteRepository.checkout("firstBranch", createBranch = true)

Expand All @@ -60,6 +72,18 @@ class JGitInteractorTest extends FlatSpec with Matchers with BeforeAndAfter with
localRepository.commits should contain theSameElementsAs Seq("Initial commit")
}

it should "checkout to given tag" in tempDirectory { localRepository =>
remoteRepository.tag("v1.0.0")

"after tag" >> (remoteRepository / "test.txt")
remoteRepository.commit("Commit after tag")

interactor.cloneRepository(remoteRepository.getAbsolutePath, localRepository) shouldBe 'success
interactor.checkoutTag(localRepository, "v1.0.0") shouldBe 'success

localRepository.commits should contain theSameElementsAs Seq("Initial commit")
}

it should "not fail if checkout existing branch" in tempDirectory { localRepository =>
remoteRepository.checkout("master")

Expand Down Expand Up @@ -101,6 +125,10 @@ class JGitInteractorTest extends FlatSpec with Matchers with BeforeAndAfter with
git.checkout.setName(name).setCreateBranch(createBranch).call()
}

def tag(name: String): Unit = withRepository { git =>
git.tag.setName(name).call()
}

private def withRepository[A](code: JGit => A): A = {
val git = JGit.open(repository)
try code(git)
Expand Down
20 changes: 10 additions & 10 deletions library/src/test/scala/giter8/JgitHelperTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ class MockRenderer extends TemplateRenderer {

class MockGit extends Git(null) {
var repository: GitRepository = _
var branch: Option[String] = _
var ref: Option[Ref] = _
var dest: File = _

override def clone(repository: GitRepository, branch: Option[String], dest: File): Try[Unit] = {
override def clone(repository: GitRepository, ref: Option[Ref], dest: File): Try[Unit] = {
this.repository = repository
this.branch = branch
this.ref = ref
Success(())
}
}
Expand All @@ -43,27 +43,27 @@ class JgitHelperTest extends FlatSpec with Matchers {
}

"JGitHelper" should "clone repo with correct URL and branch" in {
case class TestCase(config: Config, repository: GitRepository, branch: Option[String])
case class TestCase(config: Config, repository: GitRepository, ref: Option[Ref])
val testCases: Seq[TestCase] = Seq(
TestCase(
Config("file:///foo", branch = Some("baz")),
Config("file:///foo", ref = Some(Branch("baz"))),
Local("/foo"),
Some("baz")),
Some(Branch("baz"))),
TestCase(
Config("https://github.com/foo/bar.g8.git", branch = None),
Config("https://github.com/foo/bar.g8.git", ref = None),
Remote("https://github.com/foo/bar.g8.git"),
None),
TestCase(
Config("foo/bar", branch = Some("baz")),
Config("foo/bar", ref = Some(Tag("baz"))),
GitHub("foo", "bar"),
Some("baz"))
Some(Tag("baz")))
)

testCases foreach { testCase =>
new TestFixture {
helper.run(testCase.config, Seq.empty, new File("."))
git.repository shouldBe testCase.repository
git.branch shouldBe testCase.branch
git.ref shouldBe testCase.ref
}
}
}
Expand Down