├── project ├── build.properties └── plugins.sbt ├── src ├── main │ ├── resources │ │ ├── g8-build-template │ │ │ ├── description.md │ │ │ ├── __gitignore │ │ │ ├── update-g8.sh │ │ │ ├── test.sh │ │ │ └── README.md │ │ └── application.conf │ └── scala │ │ └── com │ │ └── github │ │ └── arturopala │ │ └── makeitg8 │ │ ├── EscapeCodes.scala │ │ ├── MakeItG8Config.scala │ │ ├── GitUtils.scala │ │ ├── AskUser.scala │ │ ├── CommandLine.scala │ │ ├── FileTree.scala │ │ ├── TemplateUtils.scala │ │ ├── MakeItG8Creator.scala │ │ ├── GitIgnore.scala │ │ └── MakeItG8.scala └── test │ └── scala │ └── com │ └── github │ └── arturopala │ └── makeitg8 │ ├── GitUtilsSpec.scala │ ├── FileTreeSpec.scala │ ├── TemplateUtilsSpec.scala │ └── GitIgnoreSpec.scala ├── test.sh ├── .gitignore ├── .github └── workflows │ ├── release.yml │ ├── dependency-graph.yml │ ├── build.yml │ └── publish.yml ├── .bsp └── sbt.json ├── make-it-g8.sh ├── .scalafmt.conf ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.9 2 | -------------------------------------------------------------------------------- /src/main/resources/g8-build-template/description.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sbt "run --source . --target target/test" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .makeitg8/ 3 | project/project/ 4 | project/target/ 5 | target/ 6 | .metals/ 7 | .vscode/ 8 | .bloop/ 9 | project/metals.sbt 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new version 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | release: 7 | uses: arturopala/workflows/.github/workflows/release.yml@main 8 | secrets: 9 | PAT: ${{ secrets.PAT }} 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") 3 | addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0") 4 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") 5 | -------------------------------------------------------------------------------- /.github/workflows/dependency-graph.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/dependency-graph.yml 2 | name: Update dependency graph 3 | on: 4 | push: 5 | branches: 6 | - main # default branch of the project 7 | jobs: 8 | dependency-graph: 9 | uses: arturopala/workflows/.github/workflows/dependency-graph.yml@main 10 | -------------------------------------------------------------------------------- /.bsp/sbt.json: -------------------------------------------------------------------------------- 1 | {"name":"sbt","version":"1.9.9","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/arturopala/.jenv/versions/jdk-21.0.1+12/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/arturopala/.sbtenv/versions/sbt-1.9.9/sbt/bin/sbt-launch.jar","-Dsbt.script=/Users/arturopala/.sbtenv/versions/sbt-1.9.9/sbt/bin/sbt","xsbt.boot.Boot","-bsp"]} -------------------------------------------------------------------------------- /make-it-g8.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -d .makeitg8 ]; then 4 | 5 | cd .makeitg8 6 | echo "Updating make-it-g8 repo ..." 7 | git pull origin master 8 | 9 | else 10 | 11 | mkdir .makeitg8 12 | echo "Cloning make-it-g8 repo ..." 13 | git clone https://github.com/arturopala/make-it-g8.git .makeitg8 14 | cd .makeitg8 15 | 16 | fi 17 | 18 | sbt "run $*" 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | build { 2 | source = "g8-build-template" 3 | 4 | resources = [ 5 | "__gitignore", 6 | "README.md", 7 | "test.sh" 8 | "update-g8.sh" 9 | ] 10 | 11 | test { 12 | folder = "target/sandbox" 13 | 14 | before = [ 15 | "git init", 16 | "git add .", 17 | "git commit -m start" 18 | ] 19 | 20 | command = "sbt test" 21 | } 22 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | tags: ["*"] 7 | paths-ignore: 8 | - '.github/**' 9 | - 'README.md' 10 | - 'src/docs/**' 11 | 12 | pull_request: 13 | branches: [ master, main ] 14 | 15 | workflow_dispatch: 16 | 17 | jobs: 18 | build: 19 | uses: arturopala/workflows/.github/workflows/build.yml@main 20 | -------------------------------------------------------------------------------- /src/main/resources/g8-build-template/__gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | !.scalafmt.conf 3 | *.iml 4 | *.class 5 | *.log 6 | target/ 7 | project/target/ 8 | project/boot/ 9 | dist/ 10 | boot/ 11 | logs/ 12 | out/ 13 | tmp/ 14 | bin/ 15 | projectFilesBackup/ 16 | .history 17 | .idea/ 18 | .idea_modules/ 19 | .DS_STORE 20 | .cache 21 | .settings 22 | .project 23 | .classpath 24 | version.properties 25 | RUNNING_PID 26 | node_modules/ 27 | .bloop/ 28 | .metals/ 29 | .vscode/ 30 | .bsp/ 31 | .scala-build/ -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Sonatype 2 | on: 3 | workflow_dispatch: 4 | 5 | push: 6 | tags: ["*"] 7 | 8 | jobs: 9 | publish: 10 | uses: arturopala/workflows/.github/workflows/publish.yml@main 11 | secrets: 12 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 13 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 14 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 15 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 16 | PAT: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /src/main/resources/g8-build-template/update-g8.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -d ./src/main/g8 ]]; then 4 | 5 | if ! command -v git &> /dev/null 6 | then 7 | echo "[ERROR] git command cannot be found, please install git first" 8 | exit -1 9 | fi 10 | 11 | if ! command -v sbt &> /dev/null 12 | then 13 | echo "[ERROR] sbt command cannot be found, please install sbt first" 14 | exit -1 15 | fi 16 | 17 | mkdir -p target 18 | cd target 19 | if [[ -d .makeitg8 ]] && [[ -d .makeitg8/.git ]] ; then 20 | cd .makeitg8 21 | git pull origin master 22 | else 23 | rm -r .makeitg8 24 | git clone https://github.com/arturopala/make-it-g8.git .makeitg8 25 | cd .makeitg8 26 | fi 27 | 28 | $makeItG8CommandLine$ 29 | 30 | echo "Done, updated the template based on $testTargetFolder$/$testTemplateName$" 31 | exit 0 32 | 33 | else 34 | 35 | echo "[ERROR] run the script ./update-g8.sh in the template's root folder" 36 | exit -1 37 | 38 | fi 39 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version="2.7.5" 2 | 3 | preset = defaultWithAlign 4 | maxColumn = 120 5 | lineEndings = unix 6 | importSelectors = singleLine 7 | 8 | project { 9 | git = true 10 | } 11 | 12 | align.preset = none 13 | 14 | align { 15 | tokens = [ {code = "=>", owner = "Case|Type.Arg.ByName"}, "<-", "->", "%", "%%" ] 16 | arrowEnumeratorGenerator = true 17 | openParenCallSite = false 18 | openParenDefnSite = false 19 | } 20 | 21 | binPack { 22 | parentConstructors = true 23 | } 24 | 25 | continuationIndent { 26 | callSite = 2 27 | defnSite = 2 28 | } 29 | 30 | newlines { 31 | penalizeSingleSelectMultiArgList = false 32 | sometimesBeforeColonInMethodReturnType = true 33 | alwaysBeforeElseAfterCurlyIf = true 34 | } 35 | 36 | rewrite { 37 | rules = [RedundantBraces, RedundantParens, AsciiSortImports] 38 | redundantBraces { 39 | maxLines = 100 40 | includeUnitMethods = true 41 | stringInterpolation = true 42 | } 43 | } 44 | 45 | spaces { 46 | inImportCurlyBraces = false 47 | beforeContextBoundColon = false 48 | } 49 | 50 | assumeStandardLibraryStripMargin = true -------------------------------------------------------------------------------- /src/main/resources/g8-build-template/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -d ./src/main/g8 ]]; then 4 | 5 | if ! command -v g8 &> /dev/null 6 | then 7 | echo "[ERROR] g8 command cannot be found, please install g8 following http://www.foundweekends.org/giter8/setup.html" 8 | exit -1 9 | fi 10 | 11 | export TEMPLATE=`pwd | xargs basename` 12 | 13 | echo "Creating new project $testTargetFolder$/$testTemplateName$ from the ${TEMPLATE} template ..." 14 | 15 | mkdir -p $testTargetFolder$ 16 | cd $testTargetFolder$ 17 | find . -not -name .git -delete 18 | 19 | g8 file://../../../${TEMPLATE} $g8CommandLineArgs$ "$@" 20 | 21 | if [[ -d ./$testTemplateName$ ]]; then 22 | cd $testTemplateName$ 23 | $beforeTest$ 24 | $testCommand$ 25 | echo "Done, created new project in $testTargetFolder$/$testTemplateName$" 26 | exit 0 27 | else 28 | echo "[ERROR] something went wrong, project has not been created in $testTargetFolder$/$testTemplateName$" 29 | exit -1 30 | fi 31 | 32 | else 33 | 34 | echo "[ERROR] run the script ./test.sh in the template's root folder" 35 | exit -1 36 | 37 | fi -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/EscapeCodes.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | object EscapeCodes { 20 | // colors 21 | val ANSI_RESET = "\u001B[0m" 22 | val ANSI_BLACK = "\u001B[30m" 23 | val ANSI_RED = "\u001B[31m" 24 | val ANSI_GREEN = "\u001B[32m" 25 | val ANSI_YELLOW = "\u001B[33m" 26 | val ANSI_BLUE = "\u001B[34m" 27 | val ANSI_PURPLE = "\u001B[35m" 28 | val ANSI_CYAN = "\u001B[36m" 29 | val ANSI_WHITE = "\u001B[37m" 30 | 31 | val CLEAR_PREVIOUS_LINE = "\u001b[1F\u001b[2K" 32 | val CHECK_MARK = s"$ANSI_GREEN\u2713$ANSI_RESET" 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/MakeItG8Config.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import better.files.File 20 | 21 | case class MakeItG8Config( 22 | sourceFolder: File, 23 | targetFolder: File, 24 | ignoredPaths: List[String], 25 | templateName: String, 26 | packageName: Option[String], 27 | keywordValueMap: Map[String, String], 28 | g8BuildTemplateSource: String, 29 | g8BuildTemplateResources: List[String], 30 | scriptTestTarget: String, 31 | scriptTestCommand: String, 32 | scriptBeforeTest: List[String], 33 | clearTargetFolder: Boolean, 34 | createReadme: Boolean, 35 | templateDescription: String, 36 | customReadmeHeaderPath: Option[String] 37 | ) 38 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/GitUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import scala.sys.process 20 | import scala.util.Try 21 | import java.io.File 22 | 23 | object GitUtils { 24 | 25 | val githubUrlRegex = """(?:https://|git@)github.com(?:/|:)([^/]+)/([^/]+)\.git""".r 26 | 27 | def parseGithubUrl(url: String): Option[(String, String)] = 28 | url match { 29 | case githubUrlRegex(b, c) => Some((b, c)) 30 | case _ => None 31 | } 32 | 33 | def remoteGithubUser(folder: File): Option[String] = 34 | Try( 35 | process 36 | .Process("git config --get remote.origin.url", folder) 37 | .lineStream 38 | .headOption 39 | .flatMap(parseGithubUrl) 40 | .map(_._1) 41 | ).toOption.flatten 42 | 43 | def currentBranch(folder: File): Option[String] = 44 | Try( 45 | process 46 | .Process("git branch --show-current", folder) 47 | .lineStream 48 | .headOption 49 | ).toOption.flatten 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/scala/com/github/arturopala/makeitg8/GitUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import org.scalatest.wordspec.AnyWordSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class GitUtilsSpec extends AnyWordSpec with Matchers { 23 | 24 | "GitUtils" should { 25 | "parse github URL" in { 26 | GitUtils.parseGithubUrl("") shouldBe None 27 | GitUtils.parseGithubUrl("https://github.com") shouldBe None 28 | GitUtils.parseGithubUrl("https://github.com/arturopala") shouldBe None 29 | GitUtils.parseGithubUrl("git@github.com") shouldBe None 30 | GitUtils.parseGithubUrl("git@github.com:arturopala") shouldBe None 31 | GitUtils.parseGithubUrl("https://github.com/arturopala/make-it-g8.git") shouldBe Some( 32 | ("arturopala", "make-it-g8") 33 | ) 34 | GitUtils.parseGithubUrl("git@github.com:arturopala/buffer-and-slice.git") shouldBe Some( 35 | ("arturopala", "buffer-and-slice") 36 | ) 37 | GitUtils.parseGithubUrl("https://github.com/artur.opala999/make-it-g8.git") shouldBe Some( 38 | ("artur.opala999", "make-it-g8") 39 | ) 40 | GitUtils.parseGithubUrl("git@github.com:artur-999-opala/buffer-and-slice.git") shouldBe Some( 41 | ("artur-999-opala", "buffer-and-slice") 42 | ) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/g8-build-template/README.md: -------------------------------------------------------------------------------- 1 | $templateDescription$ 2 | === 3 | 4 | A [Giter8](http://www.foundweekends.org/giter8/) template for creating $templateDescription$ 5 | 6 | $customReadmeHeader$ 7 | 8 | How to create a new project based on the template? 9 | --- 10 | 11 | * Go to directory where you want to create the template 12 | * Decide your project name (the hardest part :)) 13 | * Run the command 14 | 15 | `sbt new $templateGithubUser$/$templateName$ --branch $templateBranch$ $g8CommandLineArgs$` 16 | 17 | or 18 | 19 | * Install g8 commandline tool (http://www.foundweekends.org/giter8/setup.html) 20 | * Run the command 21 | 22 | `g8 $templateGithubUser$/$templateName$ --branch $templateBranch$ $g8CommandLineArgs$` 23 | 24 | and then 25 | 26 | cd $testTemplateName$ 27 | $beforeTest$ 28 | 29 | * Test generated project using command 30 | 31 | `$testCommand$` 32 | 33 | 34 | How to test the template and generate an example project? 35 | --- 36 | 37 | * Run `./test.sh` 38 | 39 | An example project will be then created and tested in `$testTargetFolder$/$testTemplateName$` 40 | 41 | How to modify the template? 42 | --- 43 | 44 | * review template sources in `/src/main/g8` 45 | * modify files as you need, but be careful about placeholders, paths and so on 46 | * run `./test.sh` in template root to validate your changes 47 | 48 | or (safer) ... 49 | 50 | * run `./test.sh` first 51 | * open `$testTargetFolder$/$testTemplateName$` in your preferred IDE, 52 | * modify the generated example project as you wish, 53 | * build and test it as usual, you can run `$testCommand$`, 54 | * when you are done switch back to the template root 55 | * run `./update-g8.sh` in order to port your changes back to the template. 56 | * run `./test.sh` again to validate your changes 57 | 58 | What is in the template? 59 | -- 60 | 61 | Assuming the command above 62 | the template will supply the following values for the placeholders: 63 | 64 | $placeholders$ 65 | 66 | and produce the folders and files as shown below: 67 | 68 | $exampleTargetTree$ -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/AskUser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import scala.annotation.tailrec 20 | import scala.io.StdIn 21 | 22 | trait AskUser { 23 | 24 | import EscapeCodes._ 25 | 26 | final def askYesNo(prompt: String): Boolean = 27 | ask[Boolean]( 28 | prompt, 29 | s => 30 | if (s.toLowerCase == "yes" || s.toLowerCase == "y") { 31 | print(CLEAR_PREVIOUS_LINE) 32 | Some(true) 33 | } 34 | else if (s.toLowerCase == "no" || s.toLowerCase == "n") { 35 | print(CLEAR_PREVIOUS_LINE) 36 | Some(false) 37 | } 38 | else { 39 | print(CLEAR_PREVIOUS_LINE) 40 | None 41 | } 42 | ) 43 | 44 | @tailrec 45 | final def askString(prompt: String, defaultValue: Option[String] = None): String = { 46 | val value = askOptional(prompt).orElse(defaultValue) 47 | if (value.isDefined) value.get 48 | else { 49 | print(CLEAR_PREVIOUS_LINE) 50 | askString(prompt, defaultValue) 51 | } 52 | } 53 | 54 | @tailrec 55 | final def ask[T](prompt: String, parse: String => Option[T], defaultValue: Option[T] = None): T = { 56 | val value = askOptional(prompt).flatMap(parse).orElse(defaultValue) 57 | if (value.isDefined) value.get 58 | else { 59 | print(CLEAR_PREVIOUS_LINE) 60 | ask[T](prompt, parse, defaultValue) 61 | } 62 | } 63 | 64 | final def askOptional(prompt: String): Option[String] = 65 | Option(StdIn.readLine(prompt)) 66 | .map(_.trim) 67 | .flatMap(s => if (s.isEmpty) None else Some(s)) 68 | } 69 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/CommandLine.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.nio.file.Path 20 | 21 | import org.rogach.scallop.ScallopConf 22 | import org.rogach.scallop.exceptions.{RequiredOptionNotFound, UnknownOption} 23 | 24 | import scala.util.control.NonFatal 25 | import scala.util.Try 26 | import org.rogach.scallop.ValueConverter 27 | import better.files.File 28 | 29 | class CommandLine(arguments: Seq[String]) extends ScallopConf(arguments) { 30 | 31 | import EscapeCodes._ 32 | 33 | val sourcePath = 34 | opt[Path](name = "source", short = 's', descr = "Source code path, absolute or relative") 35 | 36 | val packageName = 37 | opt[String](name = "package", short = 'p', descr = "Source code base package name") 38 | 39 | val targetPath = 40 | opt[Path](name = "target", short = 't', descr = "Template target path, absolute or relative") 41 | 42 | val templateName = 43 | opt[String](name = "name", short = 'n', descr = "Template name") 44 | 45 | val keywords = 46 | props[String](name = 'K', keyName = "placeholder", valueName = "text", descr = "Text chunks to parametrize") 47 | 48 | val templateDescription = 49 | opt[String](name = "description", short = 'd', descr = "Template description") 50 | 51 | val customReadmeHeaderPath = 52 | opt[String]( 53 | name = "custom-readme-header-path", 54 | short = 'x', 55 | descr = "Custom README.md header path", 56 | argName = "path" 57 | ) 58 | 59 | val clearBuildFiles = 60 | toggle( 61 | name = "clear", 62 | short = 'c', 63 | descrYes = "Clear target folder", 64 | descrNo = "Do not clear whole target folder, only src/main/g8 subfolder", 65 | default = Some(true) 66 | ) 67 | val createReadme = 68 | toggle( 69 | name = "readme", 70 | short = 'r', 71 | descrYes = "Create readme", 72 | descrNo = "Do not create/update readme", 73 | default = Some(true) 74 | ) 75 | 76 | val interactiveMode = 77 | toggle( 78 | name = "interactive", 79 | short = 'i', 80 | noshort = false, 81 | descrYes = "Interactive mode", 82 | default = Some(Option(System.getProperty("makeitg8.interactive")).map(_.toBoolean).getOrElse(false)) 83 | ) 84 | 85 | val forceOverwrite = 86 | toggle( 87 | name = "force", 88 | short = 'f', 89 | noshort = false, 90 | descrYes = "Force overwriting target folder", 91 | default = Some(false) 92 | ) 93 | 94 | version(s"\r\n${ANSI_YELLOW}MakeItG8$ANSI_RESET $ANSI_BLUE - convert your project into a giter8 template$ANSI_RESET") 95 | 96 | banner( 97 | s""" 98 | |${ANSI_BLUE}Usage:$ANSI_RESET sbt "run [--source {PATH}] [--target {PATH}] [--force] [--name {STRING}] [--package {STRING}] [--description {STRINGURLENCODED}] [--custom-readme-header-path {PATH}] [-K placeholder=textURLEncoded]" 99 | | 100 | |${ANSI_BLUE}Interactive mode:$ANSI_RESET sbt "run --interactive ..." 101 | | 102 | |${ANSI_BLUE}Options:$ANSI_RESET 103 | |""".stripMargin 104 | ) 105 | 106 | mainOptions = Seq(sourcePath, packageName) 107 | 108 | override def onError(e: Throwable): Unit = e match { 109 | case NonFatal(ex) => 110 | printHelp() 111 | throw new CommandLineException() 112 | } 113 | 114 | verify() 115 | } 116 | 117 | class CommandLineException extends Exception 118 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/FileTree.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.nio.file.{Path, Paths} 20 | 21 | import scala.annotation.tailrec 22 | 23 | trait FileTree { 24 | 25 | type Node = (Int, String) 26 | type Tree = List[Node] 27 | 28 | val middleNode = "├── " 29 | val endNode = "└── " 30 | val link = "│ " 31 | val space = " " * link.length 32 | 33 | private val root = Paths.get("/") 34 | 35 | def sort(paths: Seq[Path]): Seq[Path] = 36 | paths.sortWith((pl, pr) => comparePaths(pl, pr, 0)) 37 | 38 | def compute(paths: Seq[Path]): Tree = { 39 | 40 | def leafs(prefix: Path, p2: Path): Tree = 41 | (0 until p2.getNameCount).toList 42 | .map(i => (i + prefix.getNameCount, p2.getName(i).toString)) 43 | 44 | sort(paths) 45 | .foldLeft((List.empty[Node], Option.empty[Path])) { case ((tree, prevPathOpt), path) => 46 | ( 47 | prevPathOpt 48 | .map { prevPath => 49 | val (prefix, _, outstandingPath) = commonPrefix(root, prevPath, path) 50 | tree ::: leafs(prefix, outstandingPath) 51 | } 52 | .getOrElse(leafs(root, path)), 53 | Some(path) 54 | ) 55 | } 56 | ._1 57 | } 58 | 59 | def draw(pathsTree: Tree): String = { 60 | 61 | def drawLine(node: String, label: String, marks: List[Int]): (String, List[Int]) = 62 | ((0 until marks.max).map(i => if (marks.contains(i)) link else space).mkString + node + label, marks) 63 | 64 | def draw2(label: String, ls: (List[Int], String)): (String, List[Int]) = 65 | ((0 until ls._1.max).map(i => if (ls._1.contains(i)) link else space).mkString + ls._2 + label, ls._1) 66 | 67 | def append(lineWithMarks: (String, List[Int]), result: String): (String, List[Int]) = 68 | (trimRight(lineWithMarks._1) + "\n" + result, lineWithMarks._2) 69 | 70 | pathsTree.reverse 71 | .foldLeft(("", List.empty[Int])) { case ((result, marks), (offset, label)) => 72 | marks match { 73 | case Nil => drawLine(endNode, label, offset :: Nil) 74 | case head :: tail => 75 | append( 76 | if (offset == head) drawLine(middleNode, label, marks) 77 | else if (offset < head) 78 | draw2( 79 | label, 80 | tail match { 81 | case Nil => (offset :: Nil, endNode) 82 | case x :: _ if x == offset => (tail, middleNode) 83 | case _ => (offset :: tail, endNode) 84 | } 85 | ) 86 | else { 87 | val l1 = drawLine(endNode, label, offset :: marks) 88 | val l2 = drawLine(space, "", offset :: marks) 89 | (l1._1 + "\n" + l2._1, l1._2) 90 | }, 91 | result 92 | ) 93 | } 94 | } 95 | ._1 96 | } 97 | 98 | @tailrec 99 | final def comparePaths(path1: Path, path2: Path, i: Int): Boolean = { 100 | val c = path1.getName(i).toString.compareToIgnoreCase(path2.getName(i).toString) 101 | val pc1 = path1.getNameCount 102 | val pc2 = path2.getNameCount 103 | if (pc1 - 1 == i || pc2 - 1 == i) { 104 | if (c != 0) c < 0 else pc1 < pc2 105 | } 106 | else { 107 | if (c != 0) c < 0 else comparePaths(path1, path2, i + 1) 108 | } 109 | } 110 | 111 | @tailrec 112 | final def commonPrefix(prefix: Path, path1: Path, path2: Path): (Path, Path, Path) = 113 | if (path1.getNameCount > 0 && path2.getNameCount > 0) { 114 | if (path1.getName(0) != path2.getName(0)) (prefix, path1, path2) 115 | else 116 | commonPrefix( 117 | prefix.resolve(path1.subpath(0, 1)), 118 | if (path1.getNameCount == 1) path1 else path1.subpath(1, path1.getNameCount), 119 | if (path2.getNameCount == 1) path2 else path2.subpath(1, path2.getNameCount) 120 | ) 121 | } 122 | else (prefix, path1, path2) 123 | 124 | def trimRight(string: String): String = string.reverse.dropWhile(_ == ' ').reverse 125 | } 126 | 127 | object FileTree extends FileTree 128 | -------------------------------------------------------------------------------- /src/test/scala/com/github/arturopala/makeitg8/FileTreeSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.nio.file.Paths 20 | 21 | import org.scalatest.matchers.should.Matchers 22 | import org.scalatest.wordspec.AnyWordSpec 23 | 24 | class FileTreeSpec extends AnyWordSpec with Matchers { 25 | 26 | "FileTree" should { 27 | "compute a tree" in { 28 | FileTree.compute(Seq(Paths.get("test.scala"))) shouldBe List(0 -> "test.scala") 29 | FileTree.compute(Seq(Paths.get("/test", "test.scala"))) shouldBe List(0 -> "test", 1 -> "test.scala") 30 | FileTree.compute(Seq(Paths.get("/test"), Paths.get("/test", "test.scala"))) should contain 31 | .theSameElementsInOrderAs(List(0 -> "test", 1 -> "test.scala")) 32 | FileTree.compute(Seq(Paths.get("/test", "test.scala"), Paths.get("/test"))) should contain 33 | .theSameElementsInOrderAs(List(0 -> "test", 1 -> "test.scala")) 34 | FileTree.compute(Seq(Paths.get("/test", "test.scala"), Paths.get("/test"))) should contain 35 | .theSameElementsInOrderAs(List(0 -> "test", 1 -> "test.scala")) 36 | FileTree.compute(Seq(Paths.get("/test"), Paths.get("/test", "test.scala"))) should contain 37 | .theSameElementsInOrderAs(List(0 -> "test", 1 -> "test.scala")) 38 | FileTree.compute( 39 | Seq(Paths.get("/test"), Paths.get("/test", "foo", "bar.txt"), Paths.get("/test", "test.scala")) 40 | ) should contain 41 | .theSameElementsInOrderAs(List(0 -> "test", 1 -> "foo", 2 -> "bar.txt", 1 -> "test.scala")) 42 | FileTree.compute( 43 | Seq( 44 | Paths.get("/test"), 45 | Paths.get("/test", "foo", "bar.txt"), 46 | Paths.get("foo.bar"), 47 | Paths.get("/test", "test.scala") 48 | ) 49 | ) should contain 50 | .theSameElementsInOrderAs(List(0 -> "foo.bar", 0 -> "test", 1 -> "foo", 2 -> "bar.txt", 1 -> "test.scala")) 51 | FileTree.compute( 52 | Seq( 53 | Paths.get("/test"), 54 | Paths.get("/test", "foo", "bar.txt"), 55 | Paths.get("/bar", "foo.bar"), 56 | Paths.get("/test", "test.scala") 57 | ) 58 | ) should contain 59 | .theSameElementsInOrderAs( 60 | List(0 -> "bar", 1 -> "foo.bar", 0 -> "test", 1 -> "foo", 2 -> "bar.txt", 1 -> "test.scala") 61 | ) 62 | } 63 | 64 | "draw a tree 1" in { 65 | val pathTree = FileTree.compute( 66 | Seq( 67 | Paths.get("/test"), 68 | Paths.get("/test", "foo", "bar.txt"), 69 | Paths.get("foo.bar"), 70 | Paths.get("/test", "test.scala") 71 | ) 72 | ) 73 | FileTree.draw(pathTree) shouldBe 74 | """├── foo.bar 75 | |└── test 76 | | ├── foo 77 | | │ └── bar.txt 78 | | │ 79 | | └── test.scala""".stripMargin 80 | } 81 | 82 | "draw a tree 2" in { 83 | val pathTree = FileTree.compute( 84 | Seq( 85 | Paths.get("/test"), 86 | Paths.get("/test", "foo", "bar.txt"), 87 | Paths.get("/bar", "foo.bar"), 88 | Paths.get("/test", "test.scala") 89 | ) 90 | ) 91 | FileTree.draw(pathTree) shouldBe 92 | """├── bar 93 | |│ └── foo.bar 94 | |│ 95 | |└── test 96 | | ├── foo 97 | | │ └── bar.txt 98 | | │ 99 | | └── test.scala""".stripMargin 100 | } 101 | 102 | "draw a tree 3" in { 103 | val pathTree = FileTree.compute(Seq(Paths.get("/test"))) 104 | FileTree.draw(pathTree) shouldBe 105 | """└── test""".stripMargin 106 | } 107 | 108 | "draw a tree 4" in { 109 | val pathTree = FileTree.compute( 110 | Seq( 111 | Paths.get("zoo.scala"), 112 | Paths.get("/foo", "zoo", "bar.txt"), 113 | Paths.get("/bar", "foo", "foo.bar"), 114 | Paths.get("/zoo", "foo", "bar", "zoo.scala") 115 | ) 116 | ) 117 | FileTree.draw(pathTree) shouldBe 118 | """├── bar 119 | |│ └── foo 120 | |│ └── foo.bar 121 | |│ 122 | |├── foo 123 | |│ └── zoo 124 | |│ └── bar.txt 125 | |│ 126 | |├── zoo 127 | |│ └── foo 128 | |│ └── bar 129 | |│ └── zoo.scala 130 | |│ 131 | |└── zoo.scala""".stripMargin 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tool to convert your project into a giter8 template 2 | === 3 | 4 | ![Maven Central](https://img.shields.io/maven-central/v/com.github.arturopala/make-it-g8_2.13.svg) ![GitHub](https://img.shields.io/github/license/arturopala/make-it-g8.svg) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/arturopala/make-it-g8.svg) 5 | 6 | ## Demo 7 | 8 | [Watch make-it-g8 live demonstration on YouTube.](https://youtu.be/-Gd5xGiiUtI) 9 | 10 | ## Motivation 11 | Creating new [giter8](http://www.foundweekends.org/giter8) template is easy, but maintaining it later not as much. You have to do all the tedious placeholder replacing job again, and again, both in the content of the files and in the file paths. 12 | 13 | The `make-it-g8` tool provides both convenient way to create new g8 template, and to update it later multiple times without effort. 14 | 15 | ## What is g8 template? 16 | The [giter8](http://www.foundweekends.org/giter8) template is an ordinary project having nested `src/main/g8` folder where files or paths may contain placeholders, e.g. `$name$`. 17 | 18 | Place it on GitHub and call with the `g8` command line tool or `sbt new` command to spring your own project. 19 | 20 | ## Advantages of using `make-it-g8` 21 | 22 | * quick template creation with proper escaping of $ characters 23 | * easy template parametrisation with multiple placeholder values 24 | * derives automatically common placeholder variants: camel, snake, hyphen, package, packaged, etc. 25 | * generates script to generate an example project and run test on it 26 | * generates script updating the template after changes were made to the example project (covers full create-change-validate-update cycle) 27 | * generates README.md with the template usage guide and an example project filetree diagram 28 | 29 | ## Prerequisites 30 | 31 | * Java >= 8 32 | * SBT >= 1.3.x 33 | * giter8 (g8) >= 0.11.0 34 | * coursier launcher 35 | 36 | ## Usage 37 | 38 | ### Consider installing the tool locally with coursier 39 | 40 | cs install --contrib make-it-g8 41 | 42 | ### Run the tool locally in an interactive mode 43 | 44 | Run after installation using: 45 | 46 | make-it-g8 47 | 48 | or launch using coursier: 49 | 50 | cs launch com.github.arturopala:make-it-g8_2.13:1.28.0 -- --interactive 51 | 52 | or run using local clone of the repository: 53 | 54 | wget https://raw.githubusercontent.com/arturopala/make-it-g8/master/make-it-g8.sh 55 | chmod u+x make-it-g8.sh 56 | ./make-it-g8.sh --interactive 57 | 58 | ### Run the tool locally in a scripted mode 59 | 60 | Run after installation using: 61 | 62 | make-it-g8 -- --no-interactive --source {PATH} [--target {PATH}] [--name {STRING}] [--package {STRING}] [--description {STRINGURLENCODED}] [-K key=patternUrlEncoded] 63 | 64 | or launch using coursier: 65 | 66 | cs launch com.github.arturopala:make-it-g8_2.13:1.28.0 -- --source {PATH} [--target {PATH}] [--name {STRING}] [--package {STRING}] [--description {STRINGURLENCODED}] [-K key=patternUrlEncoded] 67 | 68 | or run using local clone of the repository: 69 | 70 | wget https://raw.githubusercontent.com/arturopala/make-it-g8/master/make-it-g8.sh 71 | chmod u+x make-it-g8.sh 72 | ./make-it-g8.sh --source {PATH} [--target {PATH}] [--name {STRING}] [--package {STRING}] [--description {STRINGURLENCODED}] [-K key=patternUrlEncoded] 73 | 74 | Options: 75 | 76 | -s, --source Source code path, absolute or 77 | relative 78 | -p, --package Source code base package name 79 | 80 | -c, --clear Clear target folder 81 | --noclear Do not clear whole target folder, 82 | only src/main/g8 subfolder 83 | -x, --custom-readme-header-path Custom README.md header path 84 | -d, --description Template description 85 | -f, --force Force overwriting target folder 86 | --noforce 87 | -i, --interactive Interactive mode 88 | --nointeractive 89 | -Kplaceholder=text [placeholder=text]... Text chunks to parametrize 90 | -n, --name Template name 91 | -r, --readme Create readme 92 | --noreadme Do not create/update readme 93 | -t, --target Template target path, absolute or 94 | relative 95 | -h, --help Show help message 96 | -v, --version Show version of this program 97 | 98 | ### Use as a library 99 | 100 | make-it-g8 is hosted in [The Maven Central repository](https://search.maven.org/artifact/com.github.arturopala/make-it-g8/) 101 | 102 | libraryDependencies += "com.github.arturopala" %% "make-it-g8" % "1.28.0" 103 | 104 | ## Example template created with make-it-g8 105 | 106 | * https://github.com/arturopala/cross-scala.g8 107 | * https://github.com/hmrc/template-play-frontend-fsm.g8 108 | 109 | ## Development 110 | 111 | Test 112 | 113 | sbt test 114 | 115 | Run locally 116 | 117 | # run test command 118 | ./test.sh 119 | 120 | # run test command using latest version on github 121 | ./make-it-g8.sh --source . --target target/test 122 | 123 | sbt run 124 | 125 | sbt "run --interactive" 126 | 127 | sbt run -Dmakeitg8.interactive=true 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/test/scala/com/github/arturopala/makeitg8/TemplateUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import org.scalatest.wordspec.AnyWordSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class TemplateUtilsSpec extends AnyWordSpec with Matchers { 23 | 24 | "TemplateUtils" should { 25 | "parse keyword" in { 26 | TemplateUtils.parseWord("") shouldBe Nil 27 | TemplateUtils.parseWord(" ") shouldBe Nil 28 | TemplateUtils.parseWord(" ") shouldBe Nil 29 | TemplateUtils.parseWord(" f ") shouldBe List("f") 30 | TemplateUtils.parseWord("foo") shouldBe List("foo") 31 | TemplateUtils.parseWord("fooBar") shouldBe List("foo", "Bar") 32 | TemplateUtils.parseWord("FooBar") shouldBe List("Foo", "Bar") 33 | TemplateUtils.parseWord("FooBBar") shouldBe List("Foo", "BBar") 34 | TemplateUtils.parseWord("Foo Bar") shouldBe List("Foo", "Bar") 35 | TemplateUtils.parseWord("Foo bar") shouldBe List("Foo", "bar") 36 | TemplateUtils.parseWord("Foo bar Zoo") shouldBe List("Foo", "bar", "Zoo") 37 | TemplateUtils.parseWord("Foo bar_Zoo") shouldBe List("Foo", "bar_", "Zoo") 38 | TemplateUtils.parseWord("Foo bar_zoo") shouldBe List("Foo", "bar_zoo") 39 | TemplateUtils.parseWord("Foo-bar-Zoo") shouldBe List("Foo", "bar", "Zoo") 40 | TemplateUtils.parseWord("foo-bar-zoo") shouldBe List("foo", "bar", "zoo") 41 | TemplateUtils.parseWord("9786") shouldBe List("9786") 42 | TemplateUtils.parseWord("97#86") shouldBe List("97#86") 43 | TemplateUtils.parseWord("foo9786") shouldBe List("foo", "9786") 44 | TemplateUtils.parseWord("Do It G8") shouldBe List("Do", "It", "G8") 45 | TemplateUtils.parseWord("Play27") shouldBe List("Play", "27") 46 | TemplateUtils.parseWord("Scala3") shouldBe List("Scala3") 47 | TemplateUtils.parseWord("Scala3x") shouldBe List("Scala", "3x") 48 | TemplateUtils.parseWord("Scala 2.13") shouldBe List("Scala", "2.13") 49 | } 50 | 51 | "create replacements sequence" in { 52 | TemplateUtils.prepareKeywordReplacement("key", "Foo Bar") shouldBe List( 53 | ("Foo Bar", "$key$"), 54 | ("FooBar", "$keyCamel$"), 55 | ("fooBar", "$keycamel$"), 56 | ("foobar", "$keyNoSpaceLowercase$"), 57 | ("FOOBAR", "$keyNoSpaceUppercase$"), 58 | ("foo_bar", "$keysnake$"), 59 | ("FOO_BAR", "$keySnake$"), 60 | ("Foo.Bar", "$keyPackage$"), 61 | ("foo.bar", "$keyPackageLowercase$"), 62 | ("Foo/Bar", "$keyPackaged$"), 63 | ("foo/bar", "$keyPackagedLowercase$"), 64 | ("foo-bar", "$keyHyphen$"), 65 | ("foo bar", "$keyLowercase$"), 66 | ("FOO BAR", "$keyUppercase$") 67 | ) 68 | 69 | TemplateUtils.prepareKeywordReplacement("key", "Foo") shouldBe List( 70 | ("Foo", "$key$"), 71 | ("Foo", "$keyCamel$"), 72 | ("foo", "$keycamel$"), 73 | ("foo", "$keyLowercase$"), 74 | ("FOO", "$keyUppercase$") 75 | ) 76 | 77 | TemplateUtils.prepareKeywordReplacement("key", "9786") shouldBe List( 78 | ("9786", "$key$") 79 | ) 80 | 81 | TemplateUtils.prepareKeywordReplacement("key", "foo9786") shouldBe List( 82 | ("foo9786", "$key$"), 83 | ("Foo9786", "$keyCamel$"), 84 | ("foo9786", "$keycamel$"), 85 | ("foo9786", "$keyNoSpaceLowercase$"), 86 | ("FOO9786", "$keyNoSpaceUppercase$"), 87 | ("foo_9786", "$keysnake$"), 88 | ("FOO_9786", "$keySnake$"), 89 | ("foo.9786", "$keyPackage$"), 90 | ("foo.9786", "$keyPackageLowercase$"), 91 | ("foo/9786", "$keyPackaged$"), 92 | ("foo/9786", "$keyPackagedLowercase$"), 93 | ("foo-9786", "$keyHyphen$"), 94 | ("foo 9786", "$keyLowercase$"), 95 | ("FOO 9786", "$keyUppercase$") 96 | ) 97 | 98 | TemplateUtils.prepareKeywordReplacement("key", "Do It G8") shouldBe List( 99 | ("Do It G8", "$key$"), 100 | ("DoItG8", "$keyCamel$"), 101 | ("doItG8", "$keycamel$"), 102 | ("doitg8", "$keyNoSpaceLowercase$"), 103 | ("DOITG8", "$keyNoSpaceUppercase$"), 104 | ("do_it_g8", "$keysnake$"), 105 | ("DO_IT_G8", "$keySnake$"), 106 | ("Do.It.G8", "$keyPackage$"), 107 | ("do.it.g8", "$keyPackageLowercase$"), 108 | ("Do/It/G8", "$keyPackaged$"), 109 | ("do/it/g8", "$keyPackagedLowercase$"), 110 | ("do-it-g8", "$keyHyphen$"), 111 | ("do it g8", "$keyLowercase$"), 112 | ("DO IT G8", "$keyUppercase$") 113 | ) 114 | } 115 | 116 | "create multiple replacements sequences" in { 117 | TemplateUtils.prepareKeywordsReplacements(Seq("bcde", "ABC"), Map("ABC" -> "abc")) shouldBe 118 | List( 119 | ("bcde", "$bcde$"), 120 | ("Bcde", "$bcdeCamel$"), 121 | ("bcde", "$bcdecamel$"), 122 | ("bcde", "$bcdeLowercase$"), 123 | ("BCDE", "$bcdeUppercase$"), 124 | ("abc", "$ABC$"), 125 | ("Abc", "$ABCCamel$"), 126 | ("abc", "$ABCcamel$"), 127 | ("abc", "$ABCLowercase$"), 128 | ("ABC", "$ABCUppercase$") 129 | ) 130 | } 131 | 132 | "amend the text using provided replacements" in { 133 | TemplateUtils.replace("Foo Bar", Seq("Foo" -> "$foo$")) shouldBe 134 | ("$foo$ Bar", Map("$foo$" -> 1)) 135 | TemplateUtils.replace("Foo Bar", Seq("Foo" -> "$foo$", "bar" -> "$Bar$")) shouldBe 136 | ("$foo$ Bar", Map("$foo$" -> 1, "$Bar$" -> 0)) 137 | TemplateUtils.replace("Foo Bar", Seq("Foo" -> "$foo$", "Bar" -> "$Bar$")) shouldBe 138 | ("$foo$ $Bar$", Map("$foo$" -> 1, "$Bar$" -> 1)) 139 | TemplateUtils.replace("Foo Bar", Seq("foo" -> "$Foo$", "Bar" -> "$Bar$")) shouldBe 140 | ("Foo $Bar$", Map("$Foo$" -> 0, "$Bar$" -> 1)) 141 | TemplateUtils 142 | .replace( 143 | """ 144 | |Foo 145 | |Zoo 146 | |Bar 147 | | 148 | |999 149 | """.stripMargin, 150 | Seq("Foo" -> "$foo$", "Bar" -> "$bar$") 151 | ) shouldBe 152 | (""" 153 | |$foo$ 154 | |Zoo 155 | |$bar$ 156 | | 157 | |999 158 | """.stripMargin, Map("$foo$" -> 1, "$bar$" -> 1)) 159 | } 160 | 161 | "amend the text using provided replacements when overlap" in { 162 | TemplateUtils.replace("FooFoo Bar", Seq("Foo" -> "$foo$", "FooF" -> "$foof$")) shouldBe 163 | ("$foof$oo Bar", Map("$foof$" -> 1, "$foo$" -> 0)) 164 | 165 | TemplateUtils.replace( 166 | "FooFooFoo Bar", 167 | Seq("Foo" -> "$foo$", "FooF" -> "$foof$") 168 | ) shouldBe ("$foof$oo$foo$ Bar", Map("$foo$" -> 1, "$foof$" -> 1)) 169 | 170 | TemplateUtils.replace( 171 | "FooFooFoo Bar", 172 | Seq("Foo" -> "FooFoo", "FooF" -> "FooFF") 173 | ) shouldBe ("FooFFooFooFoo Bar", Map("FooFoo" -> 1, "FooFF" -> 1)) 174 | 175 | TemplateUtils 176 | .replace( 177 | "FooBarFooFoo Bar", 178 | Seq("Foo" -> "FooBar", "FooBar" -> "Foo", "Bar" -> "Foo") 179 | ) shouldBe ("FooFooBarFooBar Foo", Map("Foo" -> 2, "FooBar" -> 2)) 180 | } 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/TemplateUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.nio.file.{Path, Paths} 20 | 21 | import scala.util.Try 22 | 23 | object TemplateUtils { 24 | 25 | //--------------------------------------- 26 | // UTILITY AND HELPER FUNCTIONS 27 | //--------------------------------------- 28 | 29 | final def templatePathFor( 30 | path: Path, 31 | replacements: Seq[(String, String)], 32 | inputStats: Map[String, Int] = Map.empty 33 | ): (Path, Map[String, Int]) = { 34 | val (s, outputStats) = replace(path.toString, replacements, inputStats) 35 | (Paths.get(s), outputStats) 36 | } 37 | 38 | final def escape(text: String): String = 39 | text 40 | .replaceAllLiterally("\\", "\\\\") 41 | .replaceAllLiterally("$", "\\$") 42 | 43 | sealed trait Part { 44 | val value: String 45 | val isReplacement: Boolean 46 | } 47 | 48 | final case class Text(value: String) extends Part { 49 | val isReplacement: Boolean = false 50 | def replace(from: String, to: String): (Seq[Part], Int) = { 51 | val i0 = value.indexOf(from) 52 | if (i0 < 0) (Seq(this), 0) 53 | else { 54 | val (seq, count) = 55 | Text(value.substring(i0 + from.length)) 56 | .replace(from, to) 57 | ( 58 | Seq( 59 | Text(value.substring(0, i0)), 60 | Replacement(to) 61 | ) ++ seq, 62 | count + 1 63 | ) 64 | } 65 | } 66 | } 67 | 68 | final case class Replacement(value: String) extends Part { 69 | val isReplacement: Boolean = true 70 | } 71 | 72 | final def replace( 73 | text: String, 74 | replacements: Seq[(String, String)], 75 | placeholderStats: Map[String, Int] = Map.empty 76 | ): (String, Map[String, Int]) = { 77 | val initial: Seq[Part] = Seq(Text(text)) 78 | val (parts, outputStats) = replacements 79 | .sortBy { case (f, _) => -f.length } 80 | .foldLeft[(Seq[Part], Map[String, Int])]((initial, placeholderStats)) { case ((seq, stats), (from, to)) => 81 | val (seq1, count) = 82 | seq.foldLeft((Seq.empty[Part], stats.getOrElse(to, 0))) { 83 | case ((s, c), r: Replacement) => (s :+ r, c) 84 | case ((s, c), t: Text) => 85 | val (s1, c1) = 86 | t.replace(from, to) 87 | (s ++ s1, c + c1) 88 | } 89 | (seq1, stats.updated(to, count)) 90 | } 91 | ( 92 | parts 93 | .map(_.value) 94 | .mkString, 95 | outputStats 96 | ) 97 | } 98 | 99 | final def prepareKeywordsReplacements( 100 | keywords: Seq[String], 101 | keywordValueMap: Map[String, String] 102 | ): Seq[(String, String)] = 103 | keywords.flatMap(k => prepareKeywordReplacement(k, keywordValueMap.getOrElse(k, k))) 104 | 105 | final def prepareKeywordReplacement(keyword: String, value: String): Seq[(String, String)] = { 106 | val parts = parseWord(value) 107 | val lowercaseParts = parts.map(lowercase) 108 | val uppercaseParts = parts.map(uppercase) 109 | 110 | if (parts.size == 1) { 111 | if (Try(parts.head.toInt).isSuccess) 112 | Seq(value -> s"$$$keyword$$") 113 | else 114 | Seq( 115 | value -> s"$$$keyword$$", 116 | lowercaseParts.map(capitalize).mkString("") -> s"$$${keyword}Camel$$", 117 | decapitalize(lowercaseParts.map(capitalize).mkString("")) -> s"$$${keyword}camel$$", 118 | lowercaseParts.mkString(" ") -> s"$$${keyword}Lowercase$$", 119 | uppercaseParts.mkString(" ") -> s"$$${keyword}Uppercase$$" 120 | ) 121 | } 122 | else 123 | Seq( 124 | value -> s"$$$keyword$$", 125 | lowercaseParts.map(capitalize).mkString("") -> s"$$${keyword}Camel$$", 126 | decapitalize(lowercaseParts.map(capitalize).mkString("")) -> s"$$${keyword}camel$$", 127 | lowercaseParts.mkString("") -> s"$$${keyword}NoSpaceLowercase$$", 128 | uppercaseParts.mkString("") -> s"$$${keyword}NoSpaceUppercase$$", 129 | lowercaseParts.mkString("_") -> s"$$${keyword}snake$$", 130 | uppercaseParts.mkString("_") -> s"$$${keyword}Snake$$", 131 | parts.mkString(".") -> s"$$${keyword}Package$$", 132 | lowercaseParts.mkString(".") -> s"$$${keyword}PackageLowercase$$", 133 | parts.mkString("/") -> s"$$${keyword}Packaged$$", 134 | lowercaseParts.mkString("/") -> s"$$${keyword}PackagedLowercase$$", 135 | lowercaseParts.mkString("-") -> s"$$${keyword}Hyphen$$", 136 | lowercaseParts.mkString(" ") -> s"$$${keyword}Lowercase$$", 137 | uppercaseParts.mkString(" ") -> s"$$${keyword}Uppercase$$" 138 | ) 139 | } 140 | 141 | final def computePlaceholders( 142 | name: String, 143 | packageName: Option[String], 144 | keywords: Seq[String], 145 | keywordValueMap: Map[String, String], 146 | placeholderStats: Map[String, Int] 147 | ): Seq[(String, String)] = { 148 | 149 | val namePropertyValue: Option[String] = 150 | if (keywords.nonEmpty) Some(s"${keywords.minBy(_.length)}Hyphen") else None 151 | 152 | val placeholderProperties: Seq[(String, String)] = 153 | keywords.map { keyword => 154 | val placeholders = Seq( 155 | s"""$keyword""" -> s"""${keywordValueMap(keyword)}""", 156 | s"""${keyword}Camel""" -> s"""$$$keyword;format="Camel"$$""", 157 | s"""${keyword}camel""" -> s"""$$$keyword;format="camel"$$""", 158 | s"""${keyword}NoSpaceLowercase""" -> s"""$$$keyword;format="camel,lowercase"$$""", 159 | s"""${keyword}NoSpaceUppercase""" -> s"""$$$keyword;format="Camel,uppercase"$$""", 160 | s"""${keyword}Snake""" -> s"""$$$keyword;format="snake,uppercase"$$""", 161 | s"""${keyword}snake""" -> s"""$$$keyword;format="snake,lowercase"$$""", 162 | s"""${keyword}Package""" -> s"""$$$keyword;format="package"$$""", 163 | s"""${keyword}PackageLowercase""" -> s"""$$$keyword;format="lowercase,package"$$""", 164 | s"""${keyword}Packaged""" -> s"""$$$keyword;format="packaged"$$""", 165 | s"""${keyword}PackagedLowercase""" -> s"""$$$keyword;format="packaged,lowercase"$$""", 166 | s"""${keyword}Hyphen""" -> s"""$$$keyword;format="normalize"$$""", 167 | s"""${keyword}Uppercase""" -> s"""$$$keyword;format="uppercase"$$""", 168 | s"""${keyword}Lowercase""" -> s"""$$$keyword;format="lowercase"$$""" 169 | ) 170 | val required = 171 | placeholders.filter { case (key, value) => 172 | placeholderStats.get(s"$$$key$$").exists(_ > 0) || 173 | namePropertyValue.contains(key) 174 | } 175 | if (required.nonEmpty && !required.exists(_._1 == keyword)) 176 | (placeholders(0) +: required) 177 | else 178 | required 179 | }.flatten 180 | 181 | val packageProperties: Seq[(String, String)] = 182 | if (packageName.isDefined) 183 | Seq("package" -> packageName.get, "packaged" -> """$package;format="packaged"$""") 184 | else 185 | Seq.empty 186 | 187 | val nameProperty: Seq[(String, String)] = 188 | Seq("name" -> namePropertyValue.map(p => s"$$$p$$").getOrElse(name)) 189 | 190 | packageProperties ++ placeholderProperties ++ nameProperty 191 | } 192 | 193 | final def parseWord(word: String): List[String] = 194 | word 195 | .foldLeft((List.empty[String], false)) { case ((list, split), ch) => 196 | if (ch == ' ' || ch == '-') (list, true) 197 | else 198 | ( 199 | list match { 200 | case Nil => s"$ch" :: Nil 201 | case head :: tail => 202 | if (split || (head.nonEmpty && shouldSplitAt(head, ch))) 203 | s"$ch" :: list 204 | else 205 | s"$ch$head" :: tail 206 | }, 207 | false 208 | ) 209 | } 210 | ._1 211 | .foldLeft(List.empty[String]) { 212 | case (Nil, part) => List(part.reverse) 213 | case (list @ (head :: tail), part) => 214 | if (head.length <= 1) 215 | (part.reverse + head) :: tail 216 | else 217 | part.reverse :: list 218 | } 219 | 220 | import Character._ 221 | final def shouldSplitAt(current: String, next: Char): Boolean = { 222 | val head = current.head 223 | (isUpperCase(next) && (!isUpperCase(head) || isDigit(head))) || 224 | (isDigit(next) && (current.length > 1) && (isUpperCase(head) || !(isDigit(head) || isPunctuation(head)))) 225 | } 226 | 227 | final def isPunctuation(ch: Char): Boolean = 228 | ch.getType >= 20 && ch.getType <= 30 229 | 230 | final def uppercase(keyword: String): String = keyword.toUpperCase 231 | final def lowercase(keyword: String): String = keyword.toLowerCase 232 | 233 | final def capitalize(keyword: String): String = 234 | keyword.take(1).toUpperCase + keyword.drop(1) 235 | 236 | final def decapitalize(keyword: String): String = 237 | keyword.take(1).toLowerCase + keyword.drop(1) 238 | } 239 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/MakeItG8Creator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.net.URLEncoder 20 | import java.nio.file.Path 21 | 22 | import better.files.{File, Resource} 23 | import java.nio.file.attribute.PosixFilePermission 24 | 25 | import scala.util.{Failure, Try} 26 | 27 | trait MakeItG8Creator { 28 | 29 | import EscapeCodes._ 30 | 31 | val KeyTemplateGithubUser = "templateGithubUser" 32 | 33 | def createG8Template(config: MakeItG8Config): Either[Throwable, Unit] = 34 | Try { 35 | 36 | println() 37 | println( 38 | s"Processing $ANSI_YELLOW${config.sourceFolder}$ANSI_RESET to giter8 template $ANSI_CYAN${config.targetFolder}$ANSI_RESET ..." 39 | ) 40 | if (config.targetFolder.exists && config.clearTargetFolder) { 41 | println( 42 | s"[${ANSI_YELLOW}warn$ANSI_RESET] Target folder exists, clearing $ANSI_YELLOW${config.targetFolder}$ANSI_RESET to make space for the new template" 43 | ) 44 | config.targetFolder.clear() 45 | } 46 | else { 47 | config.targetFolder.createDirectoryIfNotExists() 48 | } 49 | 50 | val targetG8Folder = (config.targetFolder / "src" / "main" / "g8").createDirectoryIfNotExists() 51 | if (!config.clearTargetFolder) { 52 | println( 53 | s"[${ANSI_YELLOW}warn$ANSI_RESET] Clearing $ANSI_YELLOW${targetG8Folder.path}$ANSI_RESET to make space for the new template" 54 | ) 55 | targetG8Folder.clear() 56 | } 57 | 58 | //--------------------------------------- 59 | // PREPARE CONTENT REPLACEMENT KEYWORDS 60 | //--------------------------------------- 61 | 62 | val keywords: Seq[String] = 63 | config.keywordValueMap.-(KeyTemplateGithubUser).toSeq.sortBy(p => -p._2.length).map(_._1) 64 | 65 | val contentFilesReplacements: Seq[(String, String)] = 66 | config.packageName 67 | .map(packageName => 68 | Seq( 69 | packageName.replaceAllLiterally(".", "/") -> "$packaged$", 70 | packageName -> "$package$" 71 | ) 72 | ) 73 | .getOrElse(Seq.empty) ++ TemplateUtils.prepareKeywordsReplacements( 74 | keywords, 75 | config.keywordValueMap.-(KeyTemplateGithubUser) 76 | ) 77 | 78 | println() 79 | 80 | if (contentFilesReplacements.nonEmpty) { 81 | println("Proposed content replacements:") 82 | println( 83 | contentFilesReplacements 84 | .map { case (from, to) => s"\t$ANSI_PURPLE$to$ANSI_RESET \u2192 $ANSI_CYAN$from$ANSI_RESET" } 85 | .mkString("\n") 86 | ) 87 | } 88 | 89 | println() 90 | 91 | //------------------------------------------------------ 92 | // COPY PARAMETRISED PROJECT FILES TO TEMPLATE G8 FOLDER 93 | //------------------------------------------------------ 94 | 95 | import scala.collection.JavaConverters.asScalaIterator 96 | 97 | val gitIgnore = GitIgnore(config.ignoredPaths) 98 | 99 | val (sourcePaths, placeholderStats) = config.sourceFolder.listRecursively 100 | .foldLeft((Seq.empty[Path], Map.empty[String, Int])) { case ((paths, inputStats), source) => 101 | val sourcePath: Path = 102 | config.sourceFolder.relativize(source) 103 | if (gitIgnore.isAllowed(sourcePath)) { 104 | val (targetPath, pathReplacementsStats) = 105 | TemplateUtils.templatePathFor(sourcePath, contentFilesReplacements, inputStats) 106 | val target = 107 | File(targetG8Folder.path.resolve(targetPath)) 108 | if (sourcePath == targetPath) 109 | println(s"Processing $ANSI_YELLOW$sourcePath$ANSI_RESET") 110 | else 111 | println(s"Processing $ANSI_YELLOW$sourcePath$ANSI_RESET as $ANSI_CYAN$targetPath$ANSI_RESET") 112 | if (source.isDirectory) { 113 | config.targetFolder.createDirectoryIfNotExists() 114 | (paths, inputStats) 115 | } 116 | else { 117 | target.createFileIfNotExists(createParents = true) 118 | val (template, outputStats) = 119 | TemplateUtils.replace( 120 | TemplateUtils.escape(source.contentAsString), 121 | contentFilesReplacements, 122 | pathReplacementsStats 123 | ) 124 | target.write(template) 125 | (paths :+ sourcePath, outputStats) 126 | } 127 | } 128 | else (paths, inputStats) 129 | } 130 | 131 | println() 132 | println("Template placeholder stats:") 133 | 134 | placeholderStats.foreach { 135 | case (keyword, count) if count > 0 => 136 | println(s"\t$ANSI_PURPLE$keyword$ANSI_RESET : $ANSI_GREEN$count$ANSI_RESET") 137 | case _ => 138 | } 139 | 140 | //---------------------------------------------------- 141 | // COPY PARAMETRISED BUILD FILES TO TEMPLATE G8 FOLDER 142 | //---------------------------------------------------- 143 | 144 | val placeholders: Seq[(String, String)] = 145 | TemplateUtils 146 | .computePlaceholders( 147 | config.sourceFolder.path.getFileName.toString, 148 | config.packageName, 149 | keywords, 150 | config.keywordValueMap, 151 | placeholderStats 152 | ) 153 | 154 | val buildFilesReplacements = { 155 | val testTemplateName = config.sourceFolder.name 156 | 157 | val customReadmeHeader: Option[String] = 158 | config.customReadmeHeaderPath.flatMap { path => 159 | val file = File(config.sourceFolder.path.resolve(path)) 160 | if (file.exists && file.isRegularFile) Option(file.contentAsString) 161 | else None 162 | } 163 | 164 | val customReadmeHeaderPathOpt: String = 165 | config.customReadmeHeaderPath.map(path => s"""--custom-readme-header-path "$path"""").getOrElse("") 166 | 167 | val templateGithubUser: String = config.keywordValueMap 168 | .get(KeyTemplateGithubUser) 169 | .orElse(GitUtils.remoteGithubUser(config.sourceFolder.toJava)) 170 | .orElse(GitUtils.remoteGithubUser(config.targetFolder.toJava)) 171 | .getOrElse("{GITHUB_USER}") 172 | 173 | val templateBranch: Option[String] = 174 | GitUtils 175 | .currentBranch(config.targetFolder.toJava) 176 | 177 | val keywordValueMap = 178 | Map(KeyTemplateGithubUser -> templateGithubUser) ++ config.keywordValueMap 179 | 180 | Seq( 181 | "$templateName$" -> config.templateName, 182 | "$templateDescription$" -> config.templateDescription, 183 | "$gitRepositoryName$" -> config.templateName, 184 | "$placeholders$" -> contentFilesReplacements 185 | .collect { 186 | case (value, key) if placeholders.exists { case (k, v) => s"$$$k$$" == key } => s"$key -> $value" 187 | } 188 | .mkString("\n\t"), 189 | "$exampleTargetTree$" -> FileTree.draw(FileTree.compute(sourcePaths)).linesIterator.mkString("\n\t"), 190 | "$g8CommandLineArgs$" -> s"""${(config.keywordValueMap.-(KeyTemplateGithubUser).toSeq ++ config.packageName 191 | .map(p => Seq("package" -> p)) 192 | .getOrElse(Seq.empty)) 193 | .map { case (k, v) => s"""--$k="$v"""" } 194 | .mkString(" ")} -o $testTemplateName""", 195 | "$testTargetFolder$" -> config.scriptTestTarget, 196 | "$testTemplateName$" -> testTemplateName, 197 | "$testCommand$" -> config.scriptTestCommand, 198 | "$beforeTest$" -> config.scriptBeforeTest.mkString("\n\t"), 199 | "$makeItG8CommandLine$" -> 200 | (s"""sbt "run --noclear --force --source ../../${config.scriptTestTarget}/$testTemplateName --target ../.. --name ${config.templateName} """ ++ config.packageName 201 | .map(p => s""" --package $p """) 202 | .getOrElse("") ++ s"""--description ${URLEncoder 203 | .encode(config.templateDescription, "utf-8")} $customReadmeHeaderPathOpt -K ${keywordValueMap 204 | .map { case (k, v) => 205 | s"""$k=${URLEncoder.encode(v, "utf-8")}""" 206 | } 207 | .mkString(" ")}" -Dbuild.test.command="${config.scriptTestCommand}" """), 208 | "$customReadmeHeader$" -> customReadmeHeader.getOrElse(""), 209 | "$templateGithubUser$" -> templateGithubUser, 210 | "$templateBranch$" -> templateBranch.getOrElse("master") 211 | ) 212 | } 213 | 214 | println() 215 | println("Build files replacements:") 216 | 217 | println( 218 | buildFilesReplacements 219 | .map(r => s"\t$ANSI_PURPLE${r._1}$ANSI_RESET \u2192 $ANSI_CYAN${r._2}$ANSI_RESET") 220 | .mkString("\n") 221 | ) 222 | 223 | println() 224 | 225 | config.g8BuildTemplateResources.foreach { path => 226 | if (config.createReadme || path != "README.md") { 227 | Try(Resource.my.getAsString(s"/${config.g8BuildTemplateSource}/$path")) 228 | .map { content => 229 | val targetFile = File( 230 | config.targetFolder.path 231 | .resolve(path.replace("__", ".")) 232 | ) 233 | println(s"Creating build file $ANSI_YELLOW${path.replace("__", ".")}$ANSI_RESET") 234 | targetFile.createFileIfNotExists(createParents = true) 235 | if (targetFile.name.endsWith(".sh")) { 236 | targetFile.addPermission(PosixFilePermission.OWNER_EXECUTE) 237 | } 238 | val (template, stats) = 239 | TemplateUtils.replace(content, buildFilesReplacements, Map.empty) 240 | targetFile 241 | .clear() 242 | .write(template) 243 | } 244 | .orElse { 245 | Failure(new Exception(s"Failed to create build file $path")) 246 | } 247 | } 248 | else { 249 | println(s"Skipping $ANSI_YELLOW$path$ANSI_RESET") 250 | } 251 | } 252 | 253 | //---------------------------------------------------------- 254 | // COPY OR CREATE STATIC PROJECT FILES IN TEMPLATE G8 FOLDER 255 | //---------------------------------------------------------- 256 | 257 | val defaultPropertiesFile = 258 | targetG8Folder.createChild("default.properties") 259 | 260 | defaultPropertiesFile 261 | .write( 262 | placeholders 263 | .map { case (key, value) => s"$key=$value" } 264 | .mkString("\n") 265 | ) 266 | 267 | () 268 | }.toEither 269 | } 270 | 271 | object MakeItG8Creator extends MakeItG8Creator 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/GitIgnore.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.nio.file.Path 20 | import scala.collection.JavaConverters.asScalaIterator 21 | import scala.util.matching.Regex 22 | import scala.annotation.tailrec 23 | 24 | /** Filter paths using .gitignore patterns. 25 | * 26 | * See: 27 | * - https://git-scm.com/docs/gitignore 28 | * - https://www.man7.org/linux/man-pages/man7/glob.7.html 29 | * 30 | * @note 31 | * Paths representing directories MUST end with a slash, 32 | * paths representing files MUST NOT end with a slash. 33 | */ 34 | final case class GitIgnore(gitPatterns: Seq[String]) { 35 | 36 | import GitIgnore._ 37 | 38 | private final val patterns: Seq[Pattern] = gitPatterns 39 | .map(GitIgnore.parseGitPattern) 40 | 41 | final def isIgnored(path: Path): Boolean = 42 | isIgnored( 43 | asScalaIterator(path.iterator()).map(_.toString).toIterable, 44 | path.toFile().isDirectory() 45 | ) 46 | 47 | final def isAllowed(path: Path): Boolean = 48 | !isIgnored(path) 49 | 50 | final def isIgnored(path: Path, isDirectory: Boolean): Boolean = 51 | isIgnored( 52 | asScalaIterator(path.iterator()).map(_.toString).toIterable, 53 | isDirectory 54 | ) 55 | 56 | final def isAllowed(path: Path, isDirectory: Boolean): Boolean = 57 | !isIgnored(path, isDirectory) 58 | 59 | final def isIgnored(path: Iterable[String], isDirectory: Boolean): Boolean = 60 | isIgnored( 61 | path.toSeq.mkString("/", "/", if (isDirectory) "/" else "") 62 | ) 63 | 64 | final def isAllowed(path: Iterable[String], isDirectory: Boolean): Boolean = 65 | !isIgnored(path, isDirectory) 66 | 67 | /** Path may end with slash [/] only if it denotes a directory. */ 68 | final def isIgnored(path: String): Boolean = 69 | patterns 70 | .foldLeft[Vote](Abstain)((vote, pattern) => Vote.combine(vote, pattern.isIgnored(ensureStartSlash(path)))) match { 71 | case Ignore(_) => true 72 | case Abstain | Unignore(_) => false 73 | } 74 | 75 | final def isAllowed(path: String): Boolean = 76 | !isIgnored(path) 77 | } 78 | 79 | object GitIgnore { 80 | 81 | /** Parse .gitignore file content into GitIgnore instance. */ 82 | def parse(gitIgnore: String): GitIgnore = 83 | GitIgnore(parseGitIgnore(gitIgnore)) 84 | 85 | /** Parse .gitignore file content and return sequence of patterns. */ 86 | def parseGitIgnore(gitIgnore: String): List[String] = 87 | gitIgnore.linesIterator.collect { 88 | case line if line.trim.nonEmpty && !line.startsWith("#") => 89 | removeTrailingNonEscapedSpaces(line) 90 | }.toList 91 | 92 | /** Create GitIgnore from a single well-formed pattern. */ 93 | def apply(gitPattern: String): GitIgnore = 94 | GitIgnore(Seq(gitPattern)) 95 | 96 | /** Internal model of the path pattern. */ 97 | sealed trait Pattern { 98 | def isIgnored(path: String): Vote 99 | } 100 | 101 | /** Parse single Git pattern into internal representation. */ 102 | final def parseGitPattern(p: String): Pattern = 103 | if (p.startsWith("!")) 104 | Negate(parseGitPattern(p.drop(1))) 105 | else if (p.startsWith("/") || p.dropRight(1).contains("/")) { 106 | if (p.startsWith("**/")) 107 | AnyPathPattern(ensureEndSlash(p)) 108 | else if (p.endsWith("/**")) 109 | AnyPathPattern(ensureStartSlash(p)) 110 | else { 111 | if (p.endsWith("/")) 112 | DirectoryPrefixPattern(p) 113 | else 114 | AnyPrefixPattern(p) 115 | } 116 | } 117 | else if (p.endsWith("/")) 118 | AnyDirectoryPattern(p) 119 | else 120 | AnyNamePattern(p) 121 | 122 | /** Matches any single segment of the path. */ 123 | final case class AnyNamePattern(gitPattern: String) extends Pattern { 124 | private val matcher = 125 | Matcher(ensureStartEndSlash(gitPattern)) 126 | 127 | override def isIgnored(path: String): Vote = 128 | matcher 129 | .isPartOf(ensureEndSlash(path)) 130 | .asVote 131 | } 132 | 133 | /** Matches any segments chain of the path. */ 134 | final case class AnyPathPattern(gitPattern: String) extends Pattern { 135 | private val matcher = 136 | Matcher(gitPattern) 137 | 138 | override def isIgnored(path: String): Vote = 139 | matcher 140 | .isPartOf(ensureEndSlash(path)) 141 | .asVote 142 | } 143 | 144 | /** Matches any directories chain of the path. */ 145 | final case class AnyDirectoryPattern(gitPattern: String) extends Pattern { 146 | private val matcher = 147 | Matcher(ensureStartEndSlash(gitPattern)) 148 | 149 | override def isIgnored(path: String): Vote = 150 | matcher 151 | .isPartOf(path) 152 | .asVote 153 | } 154 | 155 | /** Matches any initial segments prefix of the path. */ 156 | final case class AnyPrefixPattern(gitPattern: String) extends Pattern { 157 | private val matcher = 158 | Matcher(ensureStartEndSlash(gitPattern)) 159 | 160 | override def isIgnored(path: String): Vote = 161 | matcher 162 | .isPrefixOf(ensureEndSlash(path)) 163 | .asVote 164 | } 165 | 166 | /** Matches initial directories prefix of the path. */ 167 | final case class DirectoryPrefixPattern(gitPattern: String) extends Pattern { 168 | private val matcher = 169 | Matcher(ensureStartEndSlash(gitPattern)) 170 | 171 | override def isIgnored(path: String): Vote = 172 | matcher 173 | .isPrefixOf(path) 174 | .asVote 175 | } 176 | 177 | /** Reverts match, if any, of the nested pattern. */ 178 | final case class Negate(nestedPattern: Pattern) extends Pattern { 179 | override def isIgnored(path: String): Vote = 180 | nestedPattern.isIgnored(path) match { 181 | case Abstain => Abstain 182 | case Ignore(p) => Unignore(p) 183 | case Unignore(p) => Unignore(p) 184 | } 185 | } 186 | 187 | sealed trait Vote 188 | case class Ignore(position: Int) extends Vote 189 | case object Abstain extends Vote 190 | case class Unignore(position: Int) extends Vote 191 | 192 | object Vote { 193 | final def combine(left: Vote, right: => Vote): Vote = left match { 194 | case Abstain | Unignore(_) => right 195 | case Ignore(p1) => 196 | right match { 197 | case Abstain => left 198 | case Ignore(p2) => Ignore(Math.min(p1, p2)) 199 | case Unignore(p2) => 200 | if (p2 <= p1) Abstain else left 201 | } 202 | } 203 | } 204 | 205 | private val NONE = -1 206 | 207 | /** Matches path against the Git pattern. 208 | * 209 | * Each method returns the furthest matched position, 210 | * or -1 if not matched at all. 211 | */ 212 | sealed trait Matcher { 213 | def isPartOf(path: String): Int 214 | def isPrefixOf(path: String): Int 215 | def isSuffixOf(path: String): Int 216 | } 217 | 218 | object Matcher { 219 | def apply(gitPattern: String): Matcher = 220 | if (RegexpMatcher.isRegexpPattern(gitPattern)) 221 | RegexpMatcher(gitPattern) 222 | else 223 | LiteralMatcher(RegexpMatcher.unescape(gitPattern)) 224 | } 225 | 226 | /** Matches path literally with Git pattern. */ 227 | final case class LiteralMatcher(gitPattern: String) extends Matcher { 228 | final override def isPartOf(path: String): Int = 229 | path.endOfMatch(gitPattern) 230 | 231 | final override def isPrefixOf(path: String): Int = 232 | if (path.startsWith(gitPattern)) gitPattern.length else NONE 233 | 234 | final override def isSuffixOf(path: String): Int = 235 | if (path.endsWith(gitPattern)) path.length else NONE 236 | } 237 | 238 | /** Matches path using Git pattern compiled into Java regular expression. */ 239 | final case class RegexpMatcher(gitPattern: String) extends Matcher { 240 | final lazy val pattern = 241 | RegexpMatcher.compile(gitPattern) 242 | 243 | final override def isPartOf(path: String): Int = { 244 | val m = pattern.matcher(path) 245 | if (m.find()) m.end else NONE 246 | } 247 | 248 | final override def isPrefixOf(path: String): Int = { 249 | val m = pattern.matcher(path) 250 | if (m.find() && m.start() == 0) m.end else NONE 251 | } 252 | 253 | final override def isSuffixOf(path: String): Int = { 254 | val m = pattern.matcher(path) 255 | if (m.find() && m.end() == path.length) m.end else NONE 256 | } 257 | } 258 | 259 | object RegexpMatcher { 260 | 261 | /** Regular expression detecting if Git pattern needs regexp matcher. */ 262 | final val gitPatternRegexp: java.util.regex.Pattern = 263 | java.util.regex.Pattern.compile("""(? z) 282 | buffer 283 | .append(java.util.regex.Pattern.quote(unescape(gitPattern.substring(z, s)))) 284 | buffer.append(m match { 285 | case "*" => """[^/]*?""" 286 | case "**" => """\p{Graph}*""" 287 | case "?" => "[^/]" 288 | case s if s.startsWith("[") && s.endsWith("]") => 289 | s.replaceAllLiterally("[!", "[^") 290 | case _ => m 291 | }) 292 | z = e 293 | } 294 | if (z < gitPattern.length()) 295 | buffer 296 | .append(java.util.regex.Pattern.quote(unescape(gitPattern.substring(z, gitPattern.length())))) 297 | 298 | java.util.regex.Pattern.compile(buffer.toString) 299 | } 300 | 301 | final def unescape(s: String): String = 302 | s.replaceAllLiterally("\\*", "*") 303 | .replaceAllLiterally("\\?", "?") 304 | .replaceAllLiterally("\\[", "[") 305 | .replaceAllLiterally("\\ ", " ") 306 | } 307 | 308 | private def ensureStartSlash(s: String): String = 309 | if (s.startsWith("/")) s else "/" + s 310 | 311 | private def ensureEndSlash(s: String): String = 312 | if (s.endsWith("/")) s else s + "/" 313 | 314 | private def ensureStartEndSlash(s: String): String = 315 | ensureStartSlash(ensureEndSlash(s)) 316 | 317 | @tailrec 318 | private def removeTrailingNonEscapedSpaces(s: String): String = 319 | if (s.endsWith("\\ ")) s 320 | else if (s.endsWith(" ")) 321 | removeTrailingNonEscapedSpaces(s.dropRight(1)) 322 | else s 323 | 324 | private implicit class IntExtensions(val position: Int) extends AnyVal { 325 | final def asVote: Vote = 326 | if (position >= 0) 327 | Ignore(position) 328 | else 329 | Abstain 330 | } 331 | 332 | private implicit class StringExtensions(val string: String) extends AnyVal { 333 | final def endOfMatch(word: String): Int = { 334 | val i = string.indexOf(word) 335 | if (i < 0) NONE else i + word.length() 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/main/scala/com/github/arturopala/makeitg8/MakeItG8.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.net.URLDecoder 20 | 21 | import better.files._ 22 | import com.typesafe.config.{Config, ConfigFactory} 23 | 24 | import scala.util.Try 25 | import java.nio.charset.StandardCharsets 26 | import scala.io.StdIn 27 | import scala.annotation.tailrec 28 | import java.nio.file.Paths 29 | import java.net.URI 30 | import java.nio.file.Path 31 | import java.net.URLEncoder 32 | import scala.sys.process 33 | 34 | object MakeItG8 extends App with MakeItG8Creator with AskUser { 35 | 36 | import EscapeCodes._ 37 | 38 | readConfig().fold( 39 | { 40 | case e: CommandLineException => 41 | System.exit(-1) 42 | case e => 43 | println() 44 | println(s"$ANSI_RED\u2716 Fatal, ${e.getMessage}!$ANSI_RESET") 45 | System.exit(-1) 46 | }, 47 | config => 48 | createG8Template(config).fold( 49 | e => { 50 | println() 51 | println(s"$ANSI_RED\u2716 Fatal, ${e.getMessage}!$ANSI_RESET") 52 | System.exit(-1) 53 | }, 54 | _ => { 55 | println() 56 | println(s"$ANSI_GREEN\u2714 Done.$ANSI_RESET") 57 | System.exit(0) 58 | } 59 | ) 60 | ) 61 | 62 | def readConfig(): Either[Throwable, MakeItG8Config] = 63 | Try { 64 | 65 | import scala.collection.JavaConverters._ 66 | 67 | //--------------------------------------- 68 | // READ CONFIGURATION AND COMMAND LINE 69 | //--------------------------------------- 70 | 71 | val commandLine = new CommandLine(args) 72 | val config: Config = ConfigFactory.load() 73 | 74 | val isInteractive = commandLine.interactiveMode.getOrElse(false) 75 | 76 | println() 77 | println( 78 | s"${ANSI_YELLOW}MakeItG8$ANSI_RESET $ANSI_BLUE - convert your project into a giter8 template$ANSI_RESET" 79 | ) 80 | println() 81 | 82 | val currentDir = File.currentWorkingDirectory.path.toAbsolutePath() 83 | 84 | def resolveAndCheck(currentDir: Path, other: Path): Option[Path] = { 85 | val path = currentDir.resolve(other) 86 | if (path.toFile().exists()) Some(path) 87 | else throw new Exception(s"source folder $path does not exist") 88 | } 89 | 90 | def askSourceFolder: Path = 91 | ask[Path]( 92 | s"""${ANSI_GREEN}Select source project path, absolute or relative [$ANSI_PURPLE$currentDir$ANSI_GREEN]: $ANSI_RESET""", 93 | s => 94 | if (s.isEmpty) None 95 | else { 96 | val path = currentDir.resolve(s) 97 | if (path.toFile().exists()) Some(path) 98 | else { 99 | print(CLEAR_PREVIOUS_LINE) 100 | println(s"$ANSI_RED\u2716 Source folder $path does not exist.$ANSI_RESET") 101 | val path2 = askSourceFolder 102 | print(CLEAR_PREVIOUS_LINE) 103 | Some(path2) 104 | } 105 | }, 106 | defaultValue = Some(currentDir) 107 | ) 108 | 109 | val sourceFolder = File( 110 | commandLine.sourcePath.toOption.flatMap(resolveAndCheck(currentDir, _)).getOrElse { 111 | if (isInteractive) 112 | askSourceFolder 113 | else 114 | currentDir 115 | } 116 | ) 117 | 118 | if (isInteractive) { 119 | print(CLEAR_PREVIOUS_LINE) 120 | } 121 | println(s"$CHECK_MARK Selected source folder: $ANSI_YELLOW${sourceFolder.pathAsString}$ANSI_RESET") 122 | 123 | val defaultTarget = 124 | sourceFolder.path.resolveSibling(sourceFolder.path.getFileName() + ".g8") 125 | 126 | val targetFolder = File( 127 | commandLine.targetPath.map(currentDir.resolve).getOrElse { 128 | if (isInteractive) 129 | ask[Path]( 130 | s"""${ANSI_GREEN}Select target template path, absolute or relative [$ANSI_PURPLE$defaultTarget$ANSI_GREEN]: $ANSI_RESET""", 131 | s => 132 | if (s.isEmpty) None 133 | else { 134 | val s1 = if (s.endsWith(".g8")) s else s"$s.g8" 135 | val path = currentDir.resolve(s1) 136 | if (path.toFile().exists && !commandLine.forceOverwrite.getOrElse(false)) 137 | if ( 138 | askYesNo( 139 | s"${ANSI_GREEN}Target folder $ANSI_YELLOW${path.toString}$ANSI_GREEN exists, are you happy to overwrite it? (y/n): $ANSI_RESET" 140 | ) 141 | ) 142 | Some(path) 143 | else 144 | None 145 | else 146 | Some(path) 147 | }, 148 | defaultValue = Some(defaultTarget) 149 | ) 150 | else defaultTarget 151 | } 152 | ) 153 | 154 | if (isInteractive) { 155 | print(CLEAR_PREVIOUS_LINE) 156 | } 157 | if (targetFolder.exists && !commandLine.forceOverwrite.getOrElse(false)) { 158 | if ( 159 | !askYesNo( 160 | s"${ANSI_GREEN}Target folder $ANSI_YELLOW${targetFolder.toString}$ANSI_GREEN exists, are you happy to overwrite it? (y/n): $ANSI_RESET" 161 | ) 162 | ) { 163 | throw new Exception("cancelled by the user") 164 | } 165 | } 166 | else { 167 | println(s"$CHECK_MARK Selected target folder: $ANSI_YELLOW${targetFolder.pathAsString}$ANSI_RESET") 168 | } 169 | 170 | val templateName: String = 171 | commandLine.templateName.getOrElse { 172 | val defaultName = targetFolder.path.getFileName.toString 173 | if (isInteractive) 174 | askString( 175 | s"${ANSI_GREEN}What should be the name of the template? [$defaultName]: $ANSI_RESET", 176 | Some(defaultName) 177 | ) 178 | else 179 | defaultName 180 | } 181 | 182 | if (isInteractive) { 183 | print(CLEAR_PREVIOUS_LINE) 184 | } 185 | println(s"$CHECK_MARK Selected template name: $ANSI_YELLOW$templateName$ANSI_RESET") 186 | 187 | val packageName: Option[String] = commandLine.packageName.toOption 188 | .flatMap(p => if (p.trim.isEmpty) None else Some(p)) 189 | .orElse( 190 | if (isInteractive) 191 | askOptional(s"${ANSI_GREEN}What is your root package name? [optional] $ANSI_RESET") 192 | else { 193 | None 194 | } 195 | ) 196 | 197 | if (isInteractive) { 198 | print(CLEAR_PREVIOUS_LINE) 199 | } 200 | if (packageName.isDefined) 201 | println(s"$CHECK_MARK Selected root package name: $ANSI_YELLOW${packageName.get}$ANSI_RESET") 202 | else 203 | println(s"$CHECK_MARK No root package name has been selected$ANSI_RESET") 204 | 205 | @tailrec 206 | def askNextKeyword(map: Map[String, String]): Map[String, String] = { 207 | val word = 208 | askOptional(s"${ANSI_GREEN}Input a phrase which should be parametrized, or leave empty to skip: $ANSI_RESET") 209 | print(CLEAR_PREVIOUS_LINE) 210 | if (word.nonEmpty) { 211 | if (map.exists(_._2 == word.get)) 212 | askNextKeyword(map) 213 | else { 214 | val defaultKey = 215 | TemplateUtils.decapitalize(TemplateUtils.parseWord(word.get).map(TemplateUtils.capitalize).mkString) 216 | val key = 217 | askString( 218 | s"""${ANSI_GREEN}Select the key for "$ANSI_PURPLE${word.get}$ANSI_RESET" phrase, default [$defaultKey]: $ANSI_RESET""", 219 | Some(defaultKey) 220 | ) 221 | print(CLEAR_PREVIOUS_LINE) 222 | println(s"""\t$ANSI_CYAN$$$key$$$ANSI_GREEN \u2192 $ANSI_PURPLE${word.get}$ANSI_RESET""") 223 | askNextKeyword(map.updated(key, URLEncoder.encode(word.get, "utf-8"))) 224 | } 225 | } 226 | else map 227 | } 228 | 229 | val keywordValueMap: Map[String, String] = { 230 | val defaultKeywords = commandLine.keywords 231 | if (isInteractive) { 232 | println(s"""$ANSI_RESET Define parametrized phrases:$ANSI_RESET""") 233 | if (defaultKeywords.nonEmpty) { 234 | defaultKeywords.foreach { case (k, v) => 235 | println( 236 | s"""\t$ANSI_CYAN$$$k$$$ANSI_GREEN \u2192 $ANSI_PURPLE${URLDecoder.decode(v, "utf-8")}$ANSI_RESET""" 237 | ) 238 | } 239 | } 240 | askNextKeyword(defaultKeywords.iterator.toMap) 241 | } 242 | else defaultKeywords 243 | } 244 | 245 | val g8BuildTemplateSource = config.getString("build.source") 246 | val g8BuildTemplateResources = config.getStringList("build.resources").asScala.toList 247 | 248 | val scriptTestTarget = config.getString("build.test.folder") 249 | 250 | val scriptTestCommand = { 251 | val defaultCommand = config.getString("build.test.command") 252 | if (isInteractive) { 253 | askString( 254 | s"${ANSI_GREEN}What is the build & test command? [$defaultCommand]: $ANSI_RESET", 255 | Some(defaultCommand) 256 | ) 257 | } 258 | else defaultCommand 259 | } 260 | 261 | if (isInteractive) { 262 | print(CLEAR_PREVIOUS_LINE) 263 | } 264 | println(s"$CHECK_MARK Selected build & test command: $ANSI_YELLOW$scriptTestCommand$ANSI_RESET") 265 | 266 | def readGitIgnoreFile(file: Option[File]): List[String] = 267 | file 268 | .map(gitignore => 269 | if (gitignore.exists) { 270 | println( 271 | s"$CHECK_MARK Read .gitgnore: $ANSI_YELLOW${gitignore.path.relativize(sourceFolder)}$ANSI_RESET" 272 | ) 273 | GitIgnore.parseGitIgnore(gitignore.contentAsString(StandardCharsets.UTF_8)) 274 | } 275 | else Nil 276 | ) 277 | .getOrElse(Nil) 278 | 279 | def maybeFindGlobalGitIgnore(): Option[File] = { 280 | val globalGitIgnorePath = 281 | process.Process.apply("git config --global core.excludesFile".split(" ")).lazyLines_!.mkString 282 | if (globalGitIgnorePath.isEmpty()) None 283 | else { 284 | val globalGitIgnoreFile = File( 285 | if (globalGitIgnorePath.startsWith("~/")) 286 | s"${System.getProperty("user.home")}${globalGitIgnorePath.drop(1)}" 287 | else globalGitIgnorePath 288 | ) 289 | if (globalGitIgnoreFile.exists && globalGitIgnoreFile.isRegularFile) { 290 | Some(globalGitIgnoreFile) 291 | } 292 | else None 293 | } 294 | } 295 | 296 | val ignoredPaths: List[String] = { 297 | val localGitIgnore = Some(sourceFolder / ".gitignore") 298 | val globalGitIgnore = maybeFindGlobalGitIgnore() 299 | ".git/" :: readGitIgnoreFile(localGitIgnore) ::: readGitIgnoreFile(globalGitIgnore) 300 | } 301 | 302 | if (ignoredPaths.size > 1) { 303 | println( 304 | s"$CHECK_MARK Ignore files and folders matching the following patterns:$ANSI_RESET" 305 | ) 306 | ignoredPaths.foreach(pattern => println(s"\t$ANSI_YELLOW$pattern$ANSI_RESET")) 307 | println() 308 | } 309 | else { 310 | println( 311 | s"$ANSI_YELLOW\u2757 No .gitignore file found, processing all nested files and folders.$ANSI_RESET" 312 | ) 313 | } 314 | 315 | val proceed = 316 | if (isInteractive) 317 | askYesNo("Proceed (y/n):") 318 | else true 319 | 320 | if (proceed) 321 | MakeItG8Config( 322 | sourceFolder, 323 | targetFolder, 324 | ignoredPaths, 325 | templateName, 326 | packageName, 327 | keywordValueMap.mapValues(URLDecoder.decode(_, "utf-8")).toMap, 328 | g8BuildTemplateSource, 329 | g8BuildTemplateResources, 330 | scriptTestTarget, 331 | scriptTestCommand, 332 | config.getStringList("build.test.before").asScala.toList, 333 | commandLine.clearBuildFiles(), 334 | commandLine.createReadme(), 335 | commandLine.templateDescription 336 | .map(URLDecoder.decode(_, "utf-8")) 337 | .getOrElse(templateName), 338 | commandLine.customReadmeHeaderPath.toOption 339 | ) 340 | else { 341 | throw new Exception("cancelled by the user") 342 | } 343 | }.toEither 344 | } 345 | -------------------------------------------------------------------------------- /src/test/scala/com/github/arturopala/makeitg8/GitIgnoreSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Artur Opala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.arturopala.makeitg8 18 | 19 | import java.nio.file.Paths 20 | 21 | import org.scalatest.matchers.should.Matchers 22 | import org.scalatest.wordspec.AnyWordSpec 23 | import java.nio.file.{Path, Paths} 24 | 25 | class GitIgnoreSpec extends AnyWordSpec with Matchers { 26 | 27 | val gitIgnore1 = GitIgnore(Seq(".git", "build.sbt", "target", ".scalafmt.conf")) 28 | val gitIgnore2 = GitIgnore(Seq(".git/", "build.sbt/", "target/", ".scalafmt.conf/")) 29 | val gitIgnore3 = GitIgnore(Seq("/.git", "/build.sbt", "/target", "/.scalafmt.conf")) 30 | val gitIgnore4 = GitIgnore(Seq("project/target", "target/streams")) 31 | val gitIgnore5 = GitIgnore(Seq("project/target/", "target/streams/")) 32 | val gitIgnore6 = GitIgnore(Seq("/project/target/", "/target/streams")) 33 | val gitIgnore7 = GitIgnore(Seq("project/plugins.sbt", "resources")) 34 | 35 | "GitIgnore" should { 36 | "process top directory: target" in { 37 | val path = Paths.get("target") 38 | gitIgnore1.isIgnored(path) shouldBe true 39 | gitIgnore2.isIgnored(path) shouldBe true 40 | gitIgnore3.isIgnored(path) shouldBe true 41 | gitIgnore4.isIgnored(path) shouldBe false 42 | gitIgnore5.isIgnored(path) shouldBe false 43 | gitIgnore6.isIgnored(path) shouldBe false 44 | 45 | GitIgnore(Seq.empty).isIgnored(path) shouldBe false 46 | GitIgnore("*").isIgnored(path) shouldBe true 47 | GitIgnore("*?").isIgnored(path) shouldBe true 48 | GitIgnore("?*").isIgnored(path) shouldBe true 49 | GitIgnore("?*?").isIgnored(path) shouldBe true 50 | GitIgnore("targ*").isIgnored(path) shouldBe true 51 | GitIgnore("*arget").isIgnored(path) shouldBe true 52 | GitIgnore("target*").isIgnored(path) shouldBe true 53 | GitIgnore("*target").isIgnored(path) shouldBe true 54 | GitIgnore("tar*get").isIgnored(path) shouldBe true 55 | GitIgnore("targ*/").isIgnored(path) shouldBe true 56 | GitIgnore("*arget/").isIgnored(path) shouldBe true 57 | GitIgnore("target*/").isIgnored(path) shouldBe true 58 | GitIgnore("*target/").isIgnored(path) shouldBe true 59 | GitIgnore("tar*get/").isIgnored(path) shouldBe true 60 | GitIgnore("/targ*").isIgnored(path) shouldBe true 61 | GitIgnore("/*arget").isIgnored(path) shouldBe true 62 | GitIgnore("/target*").isIgnored(path) shouldBe true 63 | GitIgnore("/*target").isIgnored(path) shouldBe true 64 | GitIgnore("/tar*get").isIgnored(path) shouldBe true 65 | GitIgnore("/targ*/").isIgnored(path) shouldBe true 66 | GitIgnore("/*arget/").isIgnored(path) shouldBe true 67 | GitIgnore("/target*/").isIgnored(path) shouldBe true 68 | GitIgnore("/*target/").isIgnored(path) shouldBe true 69 | GitIgnore("/tar*get/").isIgnored(path) shouldBe true 70 | 71 | GitIgnore("*rg.t").isIgnored(path) shouldBe false 72 | GitIgnore("*trget").isIgnored(path) shouldBe false 73 | GitIgnore("targets*").isIgnored(path) shouldBe false 74 | GitIgnore("ar*get").isIgnored(path) shouldBe false 75 | GitIgnore("*rg.t/").isIgnored(path) shouldBe false 76 | GitIgnore("*trget/").isIgnored(path) shouldBe false 77 | GitIgnore("targets*/").isIgnored(path) shouldBe false 78 | GitIgnore("ar*get/").isIgnored(path) shouldBe false 79 | GitIgnore("/*rg.t").isIgnored(path) shouldBe false 80 | GitIgnore("/*trget").isIgnored(path) shouldBe false 81 | GitIgnore("/targets*").isIgnored(path) shouldBe false 82 | GitIgnore("/ar*get").isIgnored(path) shouldBe false 83 | GitIgnore("/*rg.t/").isIgnored(path) shouldBe false 84 | GitIgnore("/*trget/").isIgnored(path) shouldBe false 85 | GitIgnore("/targets*/").isIgnored(path) shouldBe false 86 | GitIgnore("/ar*get/").isIgnored(path) shouldBe false 87 | 88 | GitIgnore("t?rget").isIgnored(path) shouldBe true 89 | GitIgnore("?arget").isIgnored(path) shouldBe true 90 | GitIgnore("targe?").isIgnored(path) shouldBe true 91 | GitIgnore("?arge?").isIgnored(path) shouldBe true 92 | GitIgnore("t??get").isIgnored(path) shouldBe true 93 | GitIgnore("t??ge?").isIgnored(path) shouldBe true 94 | GitIgnore("???ge?").isIgnored(path) shouldBe true 95 | GitIgnore("???g??").isIgnored(path) shouldBe true 96 | GitIgnore("/t?rget").isIgnored(path) shouldBe true 97 | GitIgnore("/?arget").isIgnored(path) shouldBe true 98 | GitIgnore("/targe?").isIgnored(path) shouldBe true 99 | GitIgnore("/?arge?").isIgnored(path) shouldBe true 100 | GitIgnore("/t??get").isIgnored(path) shouldBe true 101 | GitIgnore("/t??ge?").isIgnored(path) shouldBe true 102 | GitIgnore("/???ge?").isIgnored(path) shouldBe true 103 | GitIgnore("/???g??").isIgnored(path) shouldBe true 104 | GitIgnore("/t?rget/").isIgnored(path) shouldBe true 105 | GitIgnore("/?arget/").isIgnored(path) shouldBe true 106 | GitIgnore("/targe?/").isIgnored(path) shouldBe true 107 | GitIgnore("/?arge?/").isIgnored(path) shouldBe true 108 | GitIgnore("/t??get/").isIgnored(path) shouldBe true 109 | GitIgnore("/t??ge?/").isIgnored(path) shouldBe true 110 | GitIgnore("/???ge?/").isIgnored(path) shouldBe true 111 | GitIgnore("/???g??/").isIgnored(path) shouldBe true 112 | GitIgnore("t?rget/").isIgnored(path) shouldBe true 113 | GitIgnore("?arget/").isIgnored(path) shouldBe true 114 | GitIgnore("targe?/").isIgnored(path) shouldBe true 115 | GitIgnore("?arge?/").isIgnored(path) shouldBe true 116 | GitIgnore("t??get/").isIgnored(path) shouldBe true 117 | GitIgnore("t??ge?/").isIgnored(path) shouldBe true 118 | GitIgnore("???ge?/").isIgnored(path) shouldBe true 119 | GitIgnore("???g??/").isIgnored(path) shouldBe true 120 | 121 | GitIgnore("t?get").isIgnored(path) shouldBe false 122 | GitIgnore("?arge").isIgnored(path) shouldBe false 123 | GitIgnore("tage?").isIgnored(path) shouldBe false 124 | GitIgnore("?arge").isIgnored(path) shouldBe false 125 | GitIgnore("t?get").isIgnored(path) shouldBe false 126 | GitIgnore("t??ge").isIgnored(path) shouldBe false 127 | GitIgnore("??ge?").isIgnored(path) shouldBe false 128 | GitIgnore("??g?").isIgnored(path) shouldBe false 129 | GitIgnore("/t?get").isIgnored(path) shouldBe false 130 | GitIgnore("/?arge").isIgnored(path) shouldBe false 131 | GitIgnore("/tage?").isIgnored(path) shouldBe false 132 | GitIgnore("/?arge").isIgnored(path) shouldBe false 133 | GitIgnore("/t?get").isIgnored(path) shouldBe false 134 | GitIgnore("/t??ge").isIgnored(path) shouldBe false 135 | GitIgnore("/??ge?").isIgnored(path) shouldBe false 136 | GitIgnore("/??g?").isIgnored(path) shouldBe false 137 | GitIgnore("/t?get/").isIgnored(path) shouldBe false 138 | GitIgnore("/?arge/").isIgnored(path) shouldBe false 139 | GitIgnore("/tage?/").isIgnored(path) shouldBe false 140 | GitIgnore("/?arge/").isIgnored(path) shouldBe false 141 | GitIgnore("/t?get/").isIgnored(path) shouldBe false 142 | GitIgnore("/t??ge/").isIgnored(path) shouldBe false 143 | GitIgnore("/??ge?/").isIgnored(path) shouldBe false 144 | GitIgnore("/??g?/").isIgnored(path) shouldBe false 145 | 146 | GitIgnore("[t]arget").isIgnored(path) shouldBe true 147 | GitIgnore("t[a-z]rget").isIgnored(path) shouldBe true 148 | GitIgnore("t[!b-z]rget").isIgnored(path) shouldBe true 149 | 150 | GitIgnore("t*t").isIgnored(path) shouldBe true 151 | GitIgnore("t\\*t").isIgnored(path) shouldBe false 152 | GitIgnore("ta??et").isIgnored(path) shouldBe true 153 | GitIgnore("ta\\??et").isIgnored(path) shouldBe false 154 | 155 | GitIgnore("**/target").isIgnored(path) shouldBe true 156 | GitIgnore("**/target/").isIgnored(path) shouldBe true 157 | GitIgnore("**/*").isIgnored(path) shouldBe true 158 | GitIgnore("**/*/").isIgnored(path) shouldBe true 159 | 160 | GitIgnore("tar*").isIgnored(path) shouldBe true 161 | GitIgnore("!target").isIgnored(path) shouldBe false 162 | GitIgnore(Seq("tar*", "!target")).isIgnored(path) shouldBe false 163 | GitIgnore(Seq("!target", "tar*")).isIgnored(path) shouldBe true 164 | GitIgnore(Seq("tar*", "!tar*")).isIgnored(path) shouldBe false 165 | GitIgnore(Seq("tar*", "!*get")).isIgnored(path) shouldBe false 166 | GitIgnore(Seq("!*get", "tar*")).isIgnored(path) shouldBe true 167 | GitIgnore(Seq("*", "!*")).isIgnored(path) shouldBe false 168 | 169 | GitIgnore(Seq("tar*", "!?arget", "targe?")).isIgnored(path) shouldBe true 170 | } 171 | 172 | "process top hidden directory: .git" in { 173 | val path = Paths.get(".git") 174 | gitIgnore1.isIgnored(path) shouldBe true 175 | gitIgnore2.isIgnored(path) shouldBe true 176 | gitIgnore3.isIgnored(path) shouldBe true 177 | gitIgnore4.isIgnored(path) shouldBe false 178 | gitIgnore5.isIgnored(path) shouldBe false 179 | gitIgnore6.isIgnored(path) shouldBe false 180 | 181 | GitIgnore("*").isIgnored(path) shouldBe true 182 | GitIgnore("/*").isIgnored(path) shouldBe true 183 | GitIgnore("*/").isIgnored(path) shouldBe true 184 | GitIgnore("/*/").isIgnored(path) shouldBe true 185 | GitIgnore("????").isIgnored(path) shouldBe true 186 | GitIgnore("/????").isIgnored(path) shouldBe true 187 | GitIgnore("????/").isIgnored(path) shouldBe true 188 | GitIgnore("/????/").isIgnored(path) shouldBe true 189 | 190 | GitIgnore(".g*").isIgnored(path) shouldBe true 191 | GitIgnore(".*").isIgnored(path) shouldBe true 192 | GitIgnore("*g*t").isIgnored(path) shouldBe true 193 | GitIgnore("/*g*t").isIgnored(path) shouldBe true 194 | GitIgnore("*").isIgnored(path) shouldBe true 195 | GitIgnore("?git").isIgnored(path) shouldBe true 196 | GitIgnore("?gi?").isIgnored(path) shouldBe true 197 | GitIgnore("?g??").isIgnored(path) shouldBe true 198 | GitIgnore("?g*").isIgnored(path) shouldBe true 199 | GitIgnore("/?g??").isIgnored(path) shouldBe true 200 | GitIgnore("?g*/").isIgnored(path) shouldBe true 201 | 202 | GitIgnore("*git?").isIgnored(path) shouldBe false 203 | GitIgnore(".????").isIgnored(path) shouldBe false 204 | GitIgnore("??git").isIgnored(path) shouldBe false 205 | GitIgnore(".?gi?").isIgnored(path) shouldBe false 206 | 207 | GitIgnore("**/.git").isIgnored(path) shouldBe true 208 | GitIgnore("**/.git/").isIgnored(path) shouldBe true 209 | GitIgnore("/**/.git/").isIgnored(path) shouldBe false 210 | GitIgnore("/**/.git").isIgnored(path) shouldBe false 211 | GitIgnore("**/*git").isIgnored(path) shouldBe true 212 | GitIgnore("**/?git").isIgnored(path) shouldBe true 213 | GitIgnore("**/*").isIgnored(path) shouldBe true 214 | } 215 | 216 | "process top file: build.sbt" in { 217 | val path = Paths.get("build.sbt") 218 | gitIgnore1.isIgnored(path) shouldBe true 219 | gitIgnore2.isIgnored(path) shouldBe false 220 | gitIgnore3.isIgnored(path) shouldBe true 221 | gitIgnore4.isIgnored(path) shouldBe false 222 | gitIgnore5.isIgnored(path) shouldBe false 223 | gitIgnore6.isIgnored(path) shouldBe false 224 | 225 | GitIgnore("*").isIgnored(path) shouldBe true 226 | GitIgnore("/*").isIgnored(path) shouldBe true 227 | GitIgnore("*/").isIgnored(path) shouldBe false 228 | GitIgnore("/*/").isIgnored(path) shouldBe false 229 | GitIgnore("*.???").isIgnored(path) shouldBe true 230 | GitIgnore("/*.???").isIgnored(path) shouldBe true 231 | GitIgnore("*.???/").isIgnored(path) shouldBe false 232 | GitIgnore("/*.???/").isIgnored(path) shouldBe false 233 | GitIgnore("*.sbt").isIgnored(path) shouldBe true 234 | GitIgnore("/*.sbt").isIgnored(path) shouldBe true 235 | GitIgnore("*.sbt/").isIgnored(path) shouldBe false 236 | GitIgnore("/*.sbt/").isIgnored(path) shouldBe false 237 | GitIgnore("?????.sbt").isIgnored(path) shouldBe true 238 | GitIgnore("/?????.sbt").isIgnored(path) shouldBe true 239 | GitIgnore("?????.sbt/").isIgnored(path) shouldBe false 240 | GitIgnore("/?????.sbt/").isIgnored(path) shouldBe false 241 | 242 | GitIgnore("*.????").isIgnored(path) shouldBe false 243 | GitIgnore("/*.????").isIgnored(path) shouldBe false 244 | GitIgnore("*.????/").isIgnored(path) shouldBe false 245 | GitIgnore("/*.????/").isIgnored(path) shouldBe false 246 | 247 | GitIgnore("*.sbt").isIgnored(path) shouldBe true 248 | GitIgnore("build.*").isIgnored(path) shouldBe true 249 | GitIgnore("/*.sbt").isIgnored(path) shouldBe true 250 | GitIgnore("/build.*").isIgnored(path) shouldBe true 251 | 252 | GitIgnore("/*.sbt/").isIgnored(path) shouldBe false 253 | GitIgnore("/build.*/").isIgnored(path) shouldBe false 254 | GitIgnore("*.sbt/").isIgnored(path) shouldBe false 255 | GitIgnore("build.*/").isIgnored(path) shouldBe false 256 | 257 | GitIgnore("build*sbt").isIgnored(path) shouldBe true 258 | GitIgnore("build?sbt").isIgnored(path) shouldBe true 259 | GitIgnore("buil*bt").isIgnored(path) shouldBe true 260 | GitIgnore("buil???bt").isIgnored(path) shouldBe true 261 | GitIgnore("/build*sbt").isIgnored(path) shouldBe true 262 | GitIgnore("/build?sbt").isIgnored(path) shouldBe true 263 | GitIgnore("/buil*bt").isIgnored(path) shouldBe true 264 | GitIgnore("/buil???bt").isIgnored(path) shouldBe true 265 | GitIgnore("/build*sbt/").isIgnored(path) shouldBe false 266 | GitIgnore("/build?sbt/").isIgnored(path) shouldBe false 267 | GitIgnore("/buil*bt/").isIgnored(path) shouldBe false 268 | GitIgnore("/buil???bt/").isIgnored(path) shouldBe false 269 | GitIgnore("build*sbt/").isIgnored(path) shouldBe false 270 | GitIgnore("build?sbt/").isIgnored(path) shouldBe false 271 | GitIgnore("buil*bt/").isIgnored(path) shouldBe false 272 | GitIgnore("buil???bt/").isIgnored(path) shouldBe false 273 | 274 | GitIgnore("*.sbt").isIgnored(path) shouldBe true 275 | GitIgnore("!build.sbt").isIgnored(path) shouldBe false 276 | GitIgnore(Seq("*.sbt", "!build.sbt")).isIgnored(path) shouldBe false 277 | } 278 | 279 | "process top hidden file: .scalafmt.conf" in { 280 | val path = Paths.get(".scalafmt.conf") 281 | gitIgnore1.isIgnored(path) shouldBe true 282 | gitIgnore2.isIgnored(path) shouldBe false 283 | gitIgnore3.isIgnored(path) shouldBe true 284 | gitIgnore4.isIgnored(path) shouldBe false 285 | gitIgnore5.isIgnored(path) shouldBe false 286 | gitIgnore6.isIgnored(path) shouldBe false 287 | } 288 | 289 | "process nested directory: project/target" in { 290 | val path = Paths.get("project", "target") 291 | gitIgnore1.isIgnored(path) shouldBe true 292 | gitIgnore2.isIgnored(path) shouldBe true 293 | gitIgnore3.isIgnored(path) shouldBe false 294 | gitIgnore4.isIgnored(path) shouldBe true 295 | gitIgnore5.isIgnored(path) shouldBe true 296 | gitIgnore6.isIgnored(path) shouldBe true 297 | 298 | GitIgnore("*").isIgnored(path) shouldBe true 299 | GitIgnore("*?").isIgnored(path) shouldBe true 300 | GitIgnore("?*").isIgnored(path) shouldBe true 301 | GitIgnore("?*?").isIgnored(path) shouldBe true 302 | GitIgnore("targ*").isIgnored(path) shouldBe true 303 | GitIgnore("*arget").isIgnored(path) shouldBe true 304 | GitIgnore("target*").isIgnored(path) shouldBe true 305 | GitIgnore("*target").isIgnored(path) shouldBe true 306 | GitIgnore("tar*get").isIgnored(path) shouldBe true 307 | GitIgnore("targ*/").isIgnored(path) shouldBe true 308 | GitIgnore("*arget/").isIgnored(path) shouldBe true 309 | GitIgnore("target*/").isIgnored(path) shouldBe true 310 | GitIgnore("*target/").isIgnored(path) shouldBe true 311 | GitIgnore("tar*get/").isIgnored(path) shouldBe true 312 | GitIgnore("/targ*").isIgnored(path) shouldBe false 313 | GitIgnore("/*arget").isIgnored(path) shouldBe false 314 | GitIgnore("/target*").isIgnored(path) shouldBe false 315 | GitIgnore("/*target").isIgnored(path) shouldBe false 316 | GitIgnore("/tar*get").isIgnored(path) shouldBe false 317 | GitIgnore("/targ*/").isIgnored(path) shouldBe false 318 | GitIgnore("/*arget/").isIgnored(path) shouldBe false 319 | GitIgnore("/target*/").isIgnored(path) shouldBe false 320 | GitIgnore("/*target/").isIgnored(path) shouldBe false 321 | GitIgnore("/tar*get/").isIgnored(path) shouldBe false 322 | 323 | GitIgnore("*rg.t").isIgnored(path) shouldBe false 324 | GitIgnore("*trget").isIgnored(path) shouldBe false 325 | GitIgnore("targets*").isIgnored(path) shouldBe false 326 | GitIgnore("ar*get").isIgnored(path) shouldBe false 327 | GitIgnore("*rg.t/").isIgnored(path) shouldBe false 328 | GitIgnore("*trget/").isIgnored(path) shouldBe false 329 | GitIgnore("targets*/").isIgnored(path) shouldBe false 330 | GitIgnore("ar*get/").isIgnored(path) shouldBe false 331 | GitIgnore("/*rg.t").isIgnored(path) shouldBe false 332 | GitIgnore("/*trget").isIgnored(path) shouldBe false 333 | GitIgnore("/targets*").isIgnored(path) shouldBe false 334 | GitIgnore("/ar*get").isIgnored(path) shouldBe false 335 | GitIgnore("/*rg.t/").isIgnored(path) shouldBe false 336 | GitIgnore("/*trget/").isIgnored(path) shouldBe false 337 | GitIgnore("/targets*/").isIgnored(path) shouldBe false 338 | GitIgnore("/ar*get/").isIgnored(path) shouldBe false 339 | 340 | GitIgnore("project?target").isIgnored(path) shouldBe false 341 | GitIgnore("project*target").isIgnored(path) shouldBe false 342 | GitIgnore("projec*arget").isIgnored(path) shouldBe false 343 | GitIgnore("?roject/targe*").isIgnored(path) shouldBe true 344 | 345 | GitIgnore("t?rget").isIgnored(path) shouldBe true 346 | GitIgnore("?arget").isIgnored(path) shouldBe true 347 | GitIgnore("targe?").isIgnored(path) shouldBe true 348 | GitIgnore("?arge?").isIgnored(path) shouldBe true 349 | GitIgnore("t??get").isIgnored(path) shouldBe true 350 | GitIgnore("t??ge?").isIgnored(path) shouldBe true 351 | GitIgnore("???ge?").isIgnored(path) shouldBe true 352 | GitIgnore("???g??").isIgnored(path) shouldBe true 353 | GitIgnore("/t?rget").isIgnored(path) shouldBe false 354 | GitIgnore("/?arget").isIgnored(path) shouldBe false 355 | GitIgnore("/targe?").isIgnored(path) shouldBe false 356 | GitIgnore("/?arge?").isIgnored(path) shouldBe false 357 | GitIgnore("/t??get").isIgnored(path) shouldBe false 358 | GitIgnore("/t??ge?").isIgnored(path) shouldBe false 359 | GitIgnore("/???ge?").isIgnored(path) shouldBe false 360 | GitIgnore("/???g??").isIgnored(path) shouldBe false 361 | GitIgnore("/t?rget/").isIgnored(path) shouldBe false 362 | GitIgnore("/?arget/").isIgnored(path) shouldBe false 363 | GitIgnore("/targe?/").isIgnored(path) shouldBe false 364 | GitIgnore("/?arge?/").isIgnored(path) shouldBe false 365 | GitIgnore("/t??get/").isIgnored(path) shouldBe false 366 | GitIgnore("/t??ge?/").isIgnored(path) shouldBe false 367 | GitIgnore("/???ge?/").isIgnored(path) shouldBe false 368 | GitIgnore("/???g??/").isIgnored(path) shouldBe false 369 | GitIgnore("t?rget/").isIgnored(path) shouldBe true 370 | GitIgnore("?arget/").isIgnored(path) shouldBe true 371 | GitIgnore("targe?/").isIgnored(path) shouldBe true 372 | GitIgnore("?arge?/").isIgnored(path) shouldBe true 373 | GitIgnore("t??get/").isIgnored(path) shouldBe true 374 | GitIgnore("t??ge?/").isIgnored(path) shouldBe true 375 | GitIgnore("???ge?/").isIgnored(path) shouldBe true 376 | GitIgnore("???g??/").isIgnored(path) shouldBe true 377 | } 378 | 379 | "process nested file: project/plugins.sbt" in { 380 | val path = Paths.get("project", "plugins.sbt") 381 | gitIgnore1.isIgnored(path) shouldBe false 382 | gitIgnore2.isIgnored(path) shouldBe false 383 | gitIgnore3.isIgnored(path) shouldBe false 384 | gitIgnore4.isIgnored(path) shouldBe false 385 | gitIgnore5.isIgnored(path) shouldBe false 386 | gitIgnore6.isIgnored(path) shouldBe false 387 | gitIgnore7.isIgnored(path) shouldBe true 388 | } 389 | 390 | "process nested file: src/main/resources/application.conf" in { 391 | val path = Paths.get("src", "main", "resources", "application.conf") 392 | gitIgnore1.isIgnored(path, false) shouldBe false 393 | gitIgnore2.isIgnored(path, false) shouldBe false 394 | gitIgnore3.isIgnored(path, false) shouldBe false 395 | gitIgnore4.isIgnored(path, false) shouldBe false 396 | gitIgnore5.isIgnored(path, false) shouldBe false 397 | gitIgnore6.isIgnored(path, false) shouldBe false 398 | gitIgnore7.isIgnored(path, false) shouldBe true 399 | 400 | GitIgnore("*").isIgnored(path) shouldBe true 401 | GitIgnore("/*").isIgnored(path) shouldBe true 402 | GitIgnore("*/").isIgnored(path) shouldBe true 403 | GitIgnore("/*/").isIgnored(path) shouldBe true 404 | GitIgnore("*.????").isIgnored(path) shouldBe true 405 | GitIgnore("/*.????").isIgnored(path) shouldBe false 406 | GitIgnore("*.????/").isIgnored(path) shouldBe false 407 | GitIgnore("/*.????/").isIgnored(path) shouldBe false 408 | GitIgnore("*.conf").isIgnored(path) shouldBe true 409 | GitIgnore("/*.conf").isIgnored(path) shouldBe false 410 | GitIgnore("*.conf/").isIgnored(path) shouldBe false 411 | GitIgnore("/*.conf/").isIgnored(path) shouldBe false 412 | GitIgnore("applic?????.conf").isIgnored(path) shouldBe true 413 | GitIgnore("/applic?????.conf").isIgnored(path) shouldBe false 414 | GitIgnore("applic?????.conf/").isIgnored(path) shouldBe false 415 | GitIgnore("/applic?????.conf/").isIgnored(path) shouldBe false 416 | 417 | GitIgnore("*.???").isIgnored(path) shouldBe false 418 | GitIgnore("/*.???").isIgnored(path) shouldBe false 419 | GitIgnore("*.???/").isIgnored(path) shouldBe false 420 | GitIgnore("/*.???/").isIgnored(path) shouldBe false 421 | 422 | GitIgnore("application*conf").isIgnored(path) shouldBe true 423 | GitIgnore("application?conf").isIgnored(path) shouldBe true 424 | GitIgnore("applicati*nf").isIgnored(path) shouldBe true 425 | GitIgnore("applicati?????nf").isIgnored(path) shouldBe true 426 | GitIgnore("/application*conf").isIgnored(path) shouldBe false 427 | GitIgnore("/application?conf").isIgnored(path) shouldBe false 428 | GitIgnore("/applicati*nf").isIgnored(path) shouldBe false 429 | GitIgnore("/applicati?????nf").isIgnored(path) shouldBe false 430 | GitIgnore("/application*conf/").isIgnored(path) shouldBe false 431 | GitIgnore("/application?conf/").isIgnored(path) shouldBe false 432 | GitIgnore("/applicati*nf/").isIgnored(path) shouldBe false 433 | GitIgnore("/applicati?????nf/").isIgnored(path) shouldBe false 434 | GitIgnore("application*conf/").isIgnored(path) shouldBe false 435 | GitIgnore("application?conf/").isIgnored(path) shouldBe false 436 | GitIgnore("applicati*nf/").isIgnored(path) shouldBe false 437 | GitIgnore("applicati?????nf/").isIgnored(path) shouldBe false 438 | 439 | GitIgnore("**/main").isIgnored(path) shouldBe true 440 | GitIgnore("**/main/resources/").isIgnored(path) shouldBe true 441 | GitIgnore("**/resources").isIgnored(path) shouldBe true 442 | GitIgnore("**/application.conf").isIgnored(path) shouldBe true 443 | GitIgnore("**/*.conf").isIgnored(path) shouldBe true 444 | GitIgnore("**/application.*").isIgnored(path) shouldBe true 445 | GitIgnore("**/*.conf").isIgnored(path) shouldBe true 446 | 447 | GitIgnore("**/test").isIgnored(path) shouldBe false 448 | GitIgnore("**/test/resources/").isIgnored(path) shouldBe false 449 | GitIgnore("**/scala").isIgnored(path) shouldBe false 450 | GitIgnore("**/resources.conf").isIgnored(path) shouldBe false 451 | GitIgnore("**/resources.*").isIgnored(path) shouldBe false 452 | GitIgnore("**/*.yaml").isIgnored(path) shouldBe false 453 | 454 | GitIgnore("src/**").isIgnored(path) shouldBe true 455 | GitIgnore("src/main/**").isIgnored(path) shouldBe true 456 | GitIgnore("src/main/resources/**").isIgnored(path) shouldBe true 457 | GitIgnore("src/test/**").isIgnored(path) shouldBe false 458 | GitIgnore("src/main/scala/**").isIgnored(path) shouldBe false 459 | 460 | GitIgnore("src/**/application.conf").isIgnored(path) shouldBe true 461 | GitIgnore("src/*/application.conf").isIgnored(path) shouldBe false 462 | GitIgnore("src/*/*/application.conf").isIgnored(path) shouldBe true 463 | GitIgnore("src/*/*/*.conf").isIgnored(path) shouldBe true 464 | GitIgnore("*/*/*/*.conf").isIgnored(path) shouldBe true 465 | GitIgnore("src/**/resources").isIgnored(path) shouldBe true 466 | GitIgnore("src/**/resources/application.conf").isIgnored(path) shouldBe true 467 | GitIgnore("main/**/*.conf").isIgnored(path) shouldBe false 468 | GitIgnore("src/main/**/*.conf").isIgnored(path) shouldBe true 469 | GitIgnore("test/**/*.conf").isIgnored(path) shouldBe false 470 | GitIgnore("src/test/**/*.conf").isIgnored(path) shouldBe false 471 | 472 | GitIgnore(Seq("*.conf", "!application.conf")).isIgnored(path) shouldBe false 473 | GitIgnore(Seq("*.conf", "!/src/**/application.conf")).isIgnored(path) shouldBe false 474 | GitIgnore(Seq("*.conf", "!**/application.conf")).isIgnored(path) shouldBe false 475 | GitIgnore(Seq("!application.conf", "*.conf")).isIgnored(path) shouldBe true 476 | GitIgnore(Seq("!/src/**/application.conf", "*.conf")).isIgnored(path) shouldBe true 477 | GitIgnore(Seq("!**/application.conf", "*.conf")).isIgnored(path) shouldBe true 478 | 479 | GitIgnore(Seq("!**/application.conf", "*.conf")).isIgnored(path) shouldBe true 480 | GitIgnore(Seq("src/main", "!*.conf")).isIgnored(path) shouldBe true 481 | GitIgnore(Seq("src/", "!*.conf")).isIgnored(path) shouldBe true 482 | GitIgnore(Seq("/src", "!*.conf")).isIgnored(path) shouldBe true 483 | GitIgnore(Seq("/src/", "!*.conf")).isIgnored(path) shouldBe true 484 | GitIgnore(Seq("src", "!src/main/resources")).isIgnored(path) shouldBe true 485 | GitIgnore(Seq("/src", "!src/main/resources")).isIgnored(path) shouldBe true 486 | GitIgnore(Seq("src/", "!src/main/resources")).isIgnored(path) shouldBe true 487 | GitIgnore(Seq("/src/", "!src/main/resources")).isIgnored(path) shouldBe true 488 | GitIgnore(Seq("src", "!src/main/*")).isIgnored(path) shouldBe true 489 | GitIgnore(Seq("/src", "!src/main/*")).isIgnored(path) shouldBe true 490 | GitIgnore(Seq("src/", "!src/main/*")).isIgnored(path) shouldBe true 491 | GitIgnore(Seq("/src/", "!src/main/*")).isIgnored(path) shouldBe true 492 | GitIgnore(Seq("src", "!*")).isIgnored(path) shouldBe false 493 | GitIgnore(Seq("/src", "!*")).isIgnored(path) shouldBe false 494 | GitIgnore(Seq("src/", "!*")).isIgnored(path) shouldBe false 495 | GitIgnore(Seq("/src/", "!*")).isIgnored(path) shouldBe false 496 | GitIgnore(Seq("src", "!**/")).isIgnored(path) shouldBe true 497 | GitIgnore(Seq("/src", "!**/")).isIgnored(path) shouldBe true 498 | GitIgnore(Seq("src/", "!**/")).isIgnored(path) shouldBe true 499 | GitIgnore(Seq("/src/", "!**/")).isIgnored(path) shouldBe true 500 | GitIgnore(Seq("src", "!**/*")).isIgnored(path) shouldBe true 501 | GitIgnore(Seq("/src", "!**/*")).isIgnored(path) shouldBe true 502 | GitIgnore(Seq("src/", "!**/*")).isIgnored(path) shouldBe true 503 | GitIgnore(Seq("/src/", "!**/*")).isIgnored(path) shouldBe true 504 | GitIgnore(Seq("/src/", "!/src/")).isIgnored(path) shouldBe false 505 | GitIgnore(Seq("/src/", "!src/")).isIgnored(path) shouldBe false 506 | GitIgnore(Seq("/src/", "!/src")).isIgnored(path) shouldBe false 507 | GitIgnore(Seq("/src/", "!src")).isIgnored(path) shouldBe false 508 | GitIgnore(Seq("/src/", "!src", "/src/main")).isIgnored(path) shouldBe true 509 | GitIgnore(Seq("/src/", "!src", "**/main")).isIgnored(path) shouldBe true 510 | GitIgnore(Seq("/src/", "!src", "**/main", "!main")).isIgnored(path) shouldBe false 511 | GitIgnore(Seq("**/", "!main")).isIgnored(path) shouldBe false 512 | GitIgnore(Seq("**/*", "!main")).isIgnored(path) shouldBe false 513 | GitIgnore(Seq("/src/", "!src", "**/*", "!main")).isIgnored(path) shouldBe false 514 | GitIgnore(Seq("*/", "!main")).isIgnored(path) shouldBe true 515 | GitIgnore(Seq("*/*", "!main")).isIgnored(path) shouldBe false 516 | GitIgnore(Seq("/src/", "!src", "*/*", "!main")).isIgnored(path) shouldBe false 517 | } 518 | 519 | "process nested file: foo/bar/baz/[*?].txt" in { 520 | val path = Paths.get("foo", "bar", "baz", "[*?].txt") 521 | gitIgnore1.isIgnored(path, false) shouldBe false 522 | gitIgnore2.isIgnored(path, false) shouldBe false 523 | gitIgnore3.isIgnored(path, false) shouldBe false 524 | gitIgnore4.isIgnored(path, false) shouldBe false 525 | gitIgnore5.isIgnored(path, false) shouldBe false 526 | gitIgnore6.isIgnored(path, false) shouldBe false 527 | gitIgnore7.isIgnored(path, false) shouldBe false 528 | 529 | GitIgnore("*.txt").isIgnored(path) shouldBe true 530 | GitIgnore("\\[\\*\\?].txt").isIgnored(path) shouldBe true 531 | GitIgnore("\\[??].txt").isIgnored(path) shouldBe true 532 | GitIgnore("\\[\\??].txt").isIgnored(path) shouldBe false 533 | GitIgnore("\\[*\\?].txt").isIgnored(path) shouldBe true 534 | GitIgnore("\\[*].txt").isIgnored(path) shouldBe true 535 | GitIgnore("[*].txt").isIgnored(path) shouldBe false 536 | 537 | GitIgnore("**/\\[\\*\\?].txt").isIgnored(path) shouldBe true 538 | GitIgnore("foo/**/\\[\\*\\?].txt").isIgnored(path) shouldBe true 539 | GitIgnore("foo/bar/**/\\[\\*\\?].txt").isIgnored(path) shouldBe true 540 | GitIgnore("foo/**/baz/\\[\\*\\?].txt").isIgnored(path) shouldBe true 541 | GitIgnore("foo/**/baz/**").isIgnored(path) shouldBe true 542 | GitIgnore("foo/*/baz/*").isIgnored(path) shouldBe true 543 | GitIgnore("*/*/baz/*").isIgnored(path) shouldBe true 544 | GitIgnore("*/*/*/*").isIgnored(path) shouldBe true 545 | GitIgnore("foo/*/*").isIgnored(path) shouldBe true 546 | GitIgnore("**/foo/**").isIgnored(path) shouldBe true 547 | GitIgnore("**/bar/**").isIgnored(path) shouldBe true 548 | GitIgnore("**/baz/**").isIgnored(path) shouldBe true 549 | 550 | GitIgnore("**/abc/**").isIgnored(path) shouldBe false 551 | GitIgnore("**/abc").isIgnored(path) shouldBe false 552 | GitIgnore("abc/**").isIgnored(path) shouldBe false 553 | 554 | GitIgnore 555 | .parse(""" 556 | |#foo 557 | |\[\*\?].txt 558 | |""".stripMargin) 559 | .isIgnored(path) shouldBe true 560 | 561 | GitIgnore 562 | .parse(""" 563 | |#foo 564 | |\[\*\?].txt\ 565 | |""".stripMargin) 566 | .isIgnored(path) shouldBe false 567 | 568 | GitIgnore 569 | .parse(""" 570 | |#foo 571 | | 572 | |#bar 573 | | 574 | |#baz 575 | | 576 | |""".stripMargin) 577 | .isIgnored(path) shouldBe false 578 | 579 | GitIgnore 580 | .parse(""" 581 | |#foo 582 | | 583 | |#bar 584 | |\ 585 | |#baz 586 | | 587 | |""".stripMargin) 588 | .isIgnored(path) shouldBe false 589 | 590 | GitIgnore 591 | .parse(""" 592 | |#foo 593 | | 594 | |#bar 595 | |\[\*\?].* 596 | |#baz 597 | | 598 | |""".stripMargin) 599 | .isIgnored(path) shouldBe true 600 | } 601 | } 602 | 603 | } 604 | --------------------------------------------------------------------------------