├── .tool-versions ├── project ├── build.properties ├── plugins.sbt └── Dependencies.scala ├── version.sbt ├── scala-git └── src │ ├── test │ ├── resources │ │ └── sample-repos │ │ │ ├── example.git.zip │ │ │ ├── footers.git.zip │ │ │ ├── encodings.git.zip │ │ │ ├── folder-example.git.zip │ │ │ ├── taleOfTwoBranches.git.zip │ │ │ ├── annotatedTagExample.git.zip │ │ │ └── exampleWithInitialCleanHistory.git.zip │ └── scala │ │ └── com │ │ └── madgag │ │ └── git │ │ ├── model │ │ └── TreeEntrySpec.scala │ │ ├── ReachableBlobSpec.scala │ │ ├── TreeOrBlobResolverSpec.scala │ │ └── RichTreeWalkSpec.scala │ └── main │ └── scala │ └── com │ └── madgag │ ├── diff │ ├── BeforeAndAfter.scala │ └── MapDiff.scala │ └── git │ ├── SizedObject.scala │ ├── ThreadLocalObjectDatabaseResources.scala │ ├── model │ ├── BlobFileMode.scala │ ├── FileName.scala │ └── Tree.scala │ └── package.scala ├── .idea └── copyright │ ├── profiles_settings.xml │ ├── Apache_V2.xml │ └── GPL_v3.xml ├── .gitignore ├── README.md ├── .github └── workflows │ ├── release.yml │ └── ci.yml └── scala-git-test └── src └── main └── scala └── com └── madgag └── git └── test └── package.scala /.tool-versions: -------------------------------------------------------------------------------- 1 | java corretto-21.0.3.9.1 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.4 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "7.0.5-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") 2 | 3 | addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % "3.2.1") 4 | -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/example.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/example.git.zip -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/footers.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/footers.git.zip -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/encodings.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/encodings.git.zip -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/folder-example.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/folder-example.git.zip -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/taleOfTwoBranches.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/taleOfTwoBranches.git.zip -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/annotatedTagExample.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/annotatedTagExample.git.zip -------------------------------------------------------------------------------- /scala-git/src/test/resources/sample-repos/exampleWithInitialCleanHistory.git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtyley/scala-git/HEAD/scala-git/src/test/resources/sample-repos/exampleWithInitialCleanHistory.git.zip -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/diff/BeforeAndAfter.scala: -------------------------------------------------------------------------------- 1 | package com.madgag.diff 2 | 3 | sealed trait BeforeAndAfter 4 | 5 | case object Before extends BeforeAndAfter 6 | case object After extends BeforeAndAfter -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | .idea 19 | 20 | .bsp/ 21 | test-results/ 22 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "7.3.0.202506031305-r" 6 | 7 | val scalatest = "org.scalatest" %% "scalatest" % "3.2.19" 8 | 9 | val zip4j = "net.lingala.zip4j" % "zip4j" % "2.11.5" 10 | 11 | val guava = Seq("com.google.guava" % "guava" % "33.4.8-jre", "com.google.code.findbugs" % "jsr305" % "3.0.2") 12 | 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scala-git 2 | ========= 3 | 4 | _Scala veneer for JGit_ 5 | 6 | [![scala-git artifacts](https://index.scala-lang.org/rtyley/scala-git/scala-git/latest-by-scala-version.svg)](https://index.scala-lang.org/rtyley/scala-git/scala-git/) 7 | [![Release](https://github.com/rtyley/scala-git/actions/workflows/release.yml/badge.svg)](https://github.com/rtyley/scala-git/actions/workflows/release.yml) 8 | 9 | https://www.eclipse.org/jgit/ 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | uses: guardian/gha-scala-library-release-workflow/.github/workflows/reusable-release.yml@v2 9 | permissions: { contents: write, pull-requests: write } 10 | with: 11 | GITHUB_APP_ID: 930725 12 | secrets: 13 | SONATYPE_TOKEN: ${{ secrets.AUTOMATED_MAVEN_RELEASE_SONATYPE_TOKEN }} 14 | PGP_PRIVATE_KEY: ${{ secrets.AUTOMATED_MAVEN_RELEASE_PGP_SECRET }} 15 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.AUTOMATED_MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY }} 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | 6 | # triggering CI default branch improves caching 7 | # see https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: guardian/setup-scala@v1 18 | - name: Build and Test 19 | run: sbt -v +test 20 | - name: Test Summary 21 | uses: test-summary/action@v2 22 | with: 23 | paths: "test-results/**/TEST-*.xml" 24 | if: always() -------------------------------------------------------------------------------- /.idea/copyright/Apache_V2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/git/SizedObject.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git 18 | 19 | import org.eclipse.jgit.lib.ObjectId 20 | 21 | case class SizedObject(objectId: ObjectId, size: Long) extends Ordered[SizedObject] { 22 | def compare(that: SizedObject) = size.compareTo(that.size) 23 | } -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/diff/MapDiff.scala: -------------------------------------------------------------------------------- 1 | package com.madgag.diff 2 | 3 | import com.madgag.scala.collection.decorators._ 4 | 5 | object MapDiff { 6 | def apply[K,V](before: Map[K,V], after: Map[K,V]): MapDiff[K,V] = 7 | MapDiff(Map(Before -> before, After -> after)) 8 | } 9 | 10 | case class MapDiff[K, V](beforeAndAfter: Map[BeforeAndAfter, Map[K,V]]) { 11 | 12 | lazy val commonElements: Set[K] = beforeAndAfter.values.map(_.keySet).reduce(_ intersect _) 13 | 14 | lazy val only: Map[BeforeAndAfter, Map[K,V]] = 15 | beforeAndAfter.mapV(_.view.filterKeys(!commonElements(_)).toMap).toMap 16 | 17 | lazy val (unchanged, changed) = 18 | commonElements.partition(k => beforeAndAfter(Before)(k) == beforeAndAfter(After)(k)) 19 | 20 | lazy val unchangedMap: Map[K,V] = beforeAndAfter(Before).view.filterKeys(unchanged).toMap 21 | 22 | lazy val changedMap: Map[K,Map[BeforeAndAfter, V]] = 23 | changed.map(k => k -> beforeAndAfter.mapV(_(k)).toMap).toMap 24 | 25 | } -------------------------------------------------------------------------------- /.idea/copyright/GPL_v3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/git/ThreadLocalObjectDatabaseResources.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git 18 | 19 | import org.eclipse.jgit.lib.{ObjectInserter, ObjectReader, ObjectDatabase} 20 | 21 | class ThreadLocalObjectDatabaseResources(objectDatabase: ObjectDatabase) { 22 | private lazy val _reader = new ThreadLocal[ObjectReader] { 23 | override def initialValue() = objectDatabase.newReader() 24 | } 25 | 26 | private lazy val _inserter = new ThreadLocal[ObjectInserter] { 27 | override def initialValue() = objectDatabase.newInserter() 28 | } 29 | 30 | def reader() = _reader.get 31 | 32 | def inserter() = _inserter.get 33 | } -------------------------------------------------------------------------------- /scala-git/src/test/scala/com/madgag/git/model/TreeEntrySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git.model 18 | 19 | import com.madgag.git.bfg.model.{FileName, Tree} 20 | import org.eclipse.jgit.lib.FileMode 21 | import org.eclipse.jgit.lib.FileMode._ 22 | import org.eclipse.jgit.lib.ObjectId.zeroId 23 | import org.scalatest.flatspec.AnyFlatSpec 24 | import org.scalatest.matchers.should.Matchers 25 | 26 | class TreeEntrySpec extends AnyFlatSpec with Matchers { 27 | 28 | def a(mode: FileMode, name: String) = Tree.Entry(FileName(name), mode, zeroId) 29 | 30 | "Tree entry ordering" should "match ordering used by Git" in { 31 | a(TREE, "agit-test-utils") should be < a(TREE, "agit") 32 | } 33 | } -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/git/model/BlobFileMode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git.bfg.model 18 | 19 | import org.eclipse.jgit.lib.FileMode 20 | 21 | object BlobFileMode { 22 | def apply(fileMode: FileMode): Option[BlobFileMode] = fileMode match { 23 | case FileMode.EXECUTABLE_FILE => Some(ExecutableFile) 24 | case FileMode.REGULAR_FILE => Some(RegularFile) 25 | case FileMode.SYMLINK => Some(Symlink) 26 | case _ => None 27 | } 28 | } 29 | 30 | sealed abstract class BlobFileMode(val mode: FileMode) 31 | 32 | case object ExecutableFile extends BlobFileMode(FileMode.EXECUTABLE_FILE) 33 | 34 | case object RegularFile extends BlobFileMode(FileMode.REGULAR_FILE) 35 | 36 | case object Symlink extends BlobFileMode(FileMode.SYMLINK) 37 | -------------------------------------------------------------------------------- /scala-git/src/test/scala/com/madgag/git/ReachableBlobSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git 18 | 19 | import com.madgag.git.test._ 20 | import org.eclipse.jgit.internal.storage.file.FileRepository 21 | import org.eclipse.jgit.lib.ObjectReader 22 | import org.eclipse.jgit.revwalk.RevWalk 23 | import org.scalatest.flatspec.AnyFlatSpec 24 | import org.scalatest.matchers.should.Matchers 25 | 26 | import scala.language.postfixOps 27 | 28 | class ReachableBlobSpec extends AnyFlatSpec with Matchers { 29 | 30 | implicit val repo: FileRepository = unpackRepo("/sample-repos/example.git.zip") 31 | 32 | "reachable blobs" should "match expectations" in { 33 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 34 | 35 | allBlobsReachableFrom(abbrId("475d") asRevCommit) shouldBe Set("d8d1", "34bd", "e69d", "c784", "d004").map(abbrId) 36 | } 37 | } -------------------------------------------------------------------------------- /scala-git/src/test/scala/com/madgag/git/TreeOrBlobResolverSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git 18 | 19 | import com.madgag.git.test._ 20 | import org.eclipse.jgit.internal.storage.file.FileRepository 21 | import org.eclipse.jgit.lib.ObjectReader 22 | import org.eclipse.jgit.revwalk.RevWalk 23 | import org.scalatest.EitherValues 24 | import org.scalatest.flatspec.AnyFlatSpec 25 | import org.scalatest.matchers.should.Matchers 26 | 27 | class TreeOrBlobResolverSpec extends AnyFlatSpec with Matchers with EitherValues { 28 | 29 | implicit val repo: FileRepository = unpackRepo("/sample-repos/annotatedTagExample.git.zip") 30 | 31 | "annotated tag" should "be correctly evaluated, not null" in { 32 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 33 | 34 | val annotatedTag = repo.resolve("chapter1").asRevTag 35 | 36 | treeOrBlobPointedToBy(annotatedTag).value shouldBe abbrId("4c6a") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/git/model/FileName.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git.bfg.model 18 | 19 | import org.eclipse.jgit.lib.Constants 20 | import org.eclipse.jgit.util.RawParseUtils 21 | 22 | object FileName { 23 | 24 | object ImplicitConversions { 25 | import language.implicitConversions 26 | 27 | implicit def string2FileName(str: String): FileName = FileName(str) 28 | 29 | implicit def filename2String(fileName: FileName): String = fileName.string 30 | } 31 | 32 | def apply(name: String): FileName = { 33 | require(!name.contains('/'), "File names can not contain '/'.") 34 | new FileName(Constants.encode(name)) 35 | } 36 | } 37 | 38 | class FileName(val bytes: Array[Byte]) { 39 | 40 | override def equals(that: Any): Boolean = that match { 41 | case that: FileName => (hashCode == that.hashCode) && java.util.Arrays.equals(bytes, that.bytes) 42 | case _ => false 43 | } 44 | 45 | override lazy val hashCode: Int = java.util.Arrays.hashCode(bytes) 46 | 47 | lazy val string = RawParseUtils.decode(bytes) 48 | 49 | override def toString = string 50 | 51 | } -------------------------------------------------------------------------------- /scala-git-test/src/main/scala/com/madgag/git/test/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git 18 | 19 | import net.lingala.zip4j.ZipFile 20 | import org.eclipse.jgit.internal.storage.file.FileRepository 21 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 22 | 23 | import java.io.File 24 | import java.net.URL 25 | import java.nio.file.{Files, Path} 26 | 27 | package object test { 28 | def unpackRepo(zippedRepoResource: String): FileRepository = unpackRepo(pathForResource(zippedRepoResource)) 29 | 30 | def unpackRepo(zippedRepo: Path): FileRepository = fileRepoFor(unpackRepoAndGetGitDir(zippedRepo)) 31 | 32 | def pathForResource(fileName: String, clazz: Class[_] = getClass): Path = { 33 | val resource: URL = clazz.getResource(fileName) 34 | assert(resource != null, s"Resource for $fileName is null.") 35 | new File(resource.toURI).toPath 36 | } 37 | 38 | private def unpackRepoAndGetGitDir(zippedRepo: Path): File = { 39 | assert(Files.exists(zippedRepo), s"File $zippedRepo does not exist.") 40 | 41 | val repoParentFolder = Files.createTempDirectory(s"test-${zippedRepo.getFileName}-unpacked").toFile 42 | 43 | new ZipFile(zippedRepo.toFile.getAbsolutePath).extractAll(repoParentFolder.getAbsolutePath) 44 | 45 | repoParentFolder 46 | } 47 | 48 | private def fileRepoFor(resolvedGitDir: File): FileRepository = { 49 | require(resolvedGitDir.exists) 50 | FileRepositoryBuilder.create(resolvedGitDir).asInstanceOf[FileRepository] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scala-git/src/test/scala/com/madgag/git/RichTreeWalkSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git 18 | 19 | import com.madgag.git.test._ 20 | import com.madgag.scala.collection.decorators._ 21 | import org.eclipse.jgit.internal.storage.file.FileRepository 22 | import org.eclipse.jgit.lib.ObjectReader 23 | import org.eclipse.jgit.revwalk.RevWalk 24 | import org.scalatest.flatspec.AnyFlatSpec 25 | import org.scalatest.matchers.should.Matchers 26 | 27 | import scala.collection.mutable 28 | 29 | class RichTreeWalkSpec extends AnyFlatSpec with Matchers { 30 | 31 | implicit val repo: FileRepository = unpackRepo("/sample-repos/example.git.zip") 32 | 33 | "rich tree" should "implement exists" in { 34 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 35 | 36 | val tree = abbrId("830e").asRevTree 37 | 38 | tree.walk().exists(_.getNameString == "one-kb-random") shouldBe true 39 | tree.walk().exists(_.getNameString == "chimera") shouldBe false 40 | } 41 | 42 | it should "implement map" in { 43 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 44 | 45 | val tree = abbrId("830e").asRevTree 46 | 47 | val fileNameList = tree.walk().map(_.getNameString).toList 48 | 49 | fileNameList should have size 6 50 | 51 | fileNameList.groupBy(identity).mapV(_.size).toMap should contain ("zero" -> 2) 52 | } 53 | 54 | it should "implement withFilter" in { 55 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 56 | 57 | val tree = abbrId("830e").asRevTree 58 | 59 | val filteredTreeWalk = tree.walk().withFilter(_.getNameString != "zero") 60 | 61 | val filenames = filteredTreeWalk.map(_.getNameString).toList 62 | 63 | filenames should have size 4 64 | 65 | filenames should not contain "zero" 66 | } 67 | 68 | it should "implement foreach" in { 69 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 70 | 71 | val tree = abbrId("830e").asRevTree 72 | 73 | val fileNames = mutable.Buffer[String]() 74 | 75 | tree.walk().foreach(tw => fileNames += tw.getNameString) 76 | 77 | fileNames.toList should have size 6 78 | } 79 | 80 | it should "work with for comprehensions" in { 81 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 82 | 83 | val tree = abbrId("830e").asRevTree 84 | 85 | for (t <- tree.walk()) yield t.getNameString 86 | 87 | for (t <- tree.walk()) { t.getNameString } 88 | 89 | for (t <- tree.walk() if t.getNameString == "zero") { t.getDepth } 90 | 91 | } 92 | } -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/git/model/Tree.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag.git.bfg.model 18 | 19 | import com.madgag.diff.MapDiff 20 | import org.eclipse.jgit.lib._ 21 | import org.eclipse.jgit.treewalk.CanonicalTreeParser 22 | 23 | import Constants.{OBJ_BLOB, OBJ_TREE} 24 | import scala.collection 25 | import org.eclipse.jgit.lib.FileMode.TREE 26 | 27 | object Tree { 28 | 29 | val Empty = Tree(Map.empty[FileName, (FileMode, ObjectId)]) 30 | 31 | def apply(entries: Iterable[Tree.Entry]): Tree = Tree(entries.map { 32 | entry => entry.name -> ((entry.fileMode, entry.objectId)) 33 | }.toMap) 34 | 35 | def apply(objectId: ObjectId)(implicit reader: ObjectReader): Tree = Tree(entriesFor(objectId)) 36 | 37 | def entriesFor(objectId: ObjectId)(implicit reader: ObjectReader): Seq[Entry] = { 38 | val treeParser = new CanonicalTreeParser 39 | treeParser.reset(reader, objectId) 40 | val entries = collection.mutable.Buffer[Entry]() 41 | while (!treeParser.eof) { 42 | entries += Entry(treeParser) 43 | treeParser.next() 44 | } 45 | entries.toSeq 46 | } 47 | 48 | case class Entry(name: FileName, fileMode: FileMode, objectId: ObjectId) extends Ordered[Entry] { 49 | 50 | def compare(that: Entry) = pathCompare(name.bytes, that.name.bytes, fileMode, that.fileMode) 51 | 52 | private def pathCompare(a: Array[Byte], b: Array[Byte], aMode: FileMode, bMode: FileMode): Int = { 53 | 54 | def lastPathChar(mode: FileMode): Int = if (mode == TREE) '/' else '\u0000' 55 | 56 | var pos = 0 57 | while (pos < a.length && pos < b.length) { 58 | val cmp: Int = (a(pos) & 0xff) - (b(pos) & 0xff) 59 | if (cmp != 0) return cmp 60 | pos += 1 61 | } 62 | 63 | if (pos < a.length) { 64 | (a(pos) & 0xff) - lastPathChar(bMode) 65 | } else if (pos < b.length) { 66 | lastPathChar(aMode) - (b(pos) & 0xff) 67 | } else { 68 | 0 69 | } 70 | } 71 | } 72 | 73 | object Entry { 74 | 75 | def apply(treeParser: CanonicalTreeParser): Entry = { 76 | val nameBuff = new Array[Byte](treeParser.getNameLength) 77 | treeParser.getName(nameBuff, 0) 78 | 79 | Entry(new FileName(nameBuff), treeParser.getEntryFileMode, treeParser.getEntryObjectId) 80 | } 81 | 82 | } 83 | 84 | trait EntryGrouping { 85 | def treeEntries: Iterable[Tree.Entry] 86 | } 87 | 88 | } 89 | 90 | case class Tree(entryMap: Map[FileName, (FileMode, ObjectId)]) { 91 | 92 | protected def repr = this 93 | 94 | lazy val entries: Iterable[Tree.Entry] = entryMap.map { 95 | case (name, (fileMode, objectId)) => Tree.Entry(name, fileMode, objectId) 96 | } 97 | 98 | lazy val entriesByType = entries.groupBy(_.fileMode.getObjectType).withDefaultValue(Seq.empty) 99 | 100 | lazy val sortedEntries = entries.toList.sorted 101 | 102 | def formatter: TreeFormatter = { 103 | val treeFormatter = new TreeFormatter() 104 | sortedEntries.foreach(e => treeFormatter.append(e.name.bytes, e.fileMode, e.objectId)) 105 | 106 | treeFormatter 107 | } 108 | 109 | lazy val objectId = formatter.computeId(new ObjectInserter.Formatter) 110 | 111 | lazy val blobs = TreeBlobs(entriesByType(OBJ_BLOB).flatMap { 112 | e => BlobFileMode(e.fileMode).map { 113 | blobFileMode => e.name -> ((blobFileMode, e.objectId)) 114 | } 115 | }.toMap) 116 | 117 | lazy val subtrees = TreeSubtrees(entriesByType(OBJ_TREE).map { 118 | e => e.name -> e.objectId 119 | }.toMap) 120 | 121 | def copyWith(subtrees: TreeSubtrees, blobs: TreeBlobs): Tree = { 122 | val otherEntries = (entriesByType - OBJ_BLOB - OBJ_TREE).values.flatten 123 | Tree(blobs.treeEntries ++ subtrees.treeEntries ++ otherEntries) 124 | } 125 | 126 | } 127 | 128 | case class TreeBlobEntry(filename: FileName, mode: BlobFileMode, objectId: ObjectId) { 129 | lazy val toTreeEntry = Tree.Entry(filename, mode.mode, objectId) 130 | 131 | lazy val withoutName: (BlobFileMode, ObjectId) = (mode, objectId) 132 | } 133 | 134 | object TreeBlobs { 135 | import language.implicitConversions 136 | 137 | implicit def entries2Object(entries: Iterable[TreeBlobEntry]): TreeBlobs = TreeBlobs(entries) 138 | 139 | def apply(entries: Iterable[TreeBlobEntry]): TreeBlobs = 140 | TreeBlobs(entries.map(e => e.filename -> ((e.mode, e.objectId))).toMap) 141 | } 142 | 143 | case class TreeBlobs(entryMap: Map[FileName, (BlobFileMode, ObjectId)]) extends Tree.EntryGrouping { 144 | 145 | lazy val entries: Iterable[TreeBlobEntry] = entryMap.map { 146 | case (name, (blobFileMode, objectId)) => TreeBlobEntry(name, blobFileMode, objectId) 147 | } 148 | 149 | lazy val treeEntries: Iterable[Tree.Entry] = entries.map(_.toTreeEntry) 150 | 151 | def objectId(fileName: FileName) = entryMap.get(fileName).map(_._2) 152 | 153 | def diff(otherTreeBlobs: TreeBlobs) = MapDiff(entryMap, otherTreeBlobs.entryMap) 154 | } 155 | 156 | case class TreeSubtrees(entryMap: Map[FileName, ObjectId]) extends Tree.EntryGrouping { 157 | 158 | lazy val treeEntries = entryMap.map { 159 | case (name, objectId) => Tree.Entry(name, TREE, objectId) 160 | } 161 | 162 | lazy val withoutEmptyTrees = TreeSubtrees(entryMap.filterNot(_._2 == Tree.Empty.objectId)) 163 | } -------------------------------------------------------------------------------- /scala-git/src/main/scala/com/madgag/git/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Roberto Tyley 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.madgag 18 | 19 | import java.io.File 20 | import java.nio.charset.Charset 21 | import _root_.scala.jdk.CollectionConverters._ 22 | import _root_.scala.annotation.tailrec 23 | import _root_.scala.language.implicitConversions 24 | import _root_.scala.util.{Success, Try} 25 | import org.eclipse.jgit 26 | import org.eclipse.jgit.api.Git 27 | import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm 28 | import org.eclipse.jgit.diff._ 29 | import org.eclipse.jgit.internal.storage.file.ObjectDirectory 30 | import org.eclipse.jgit.lib.Constants.{OBJ_BLOB, R_HEADS} 31 | import org.eclipse.jgit.lib.ObjectInserter.Formatter 32 | import org.eclipse.jgit.lib.ObjectReader.OBJ_ANY 33 | import org.eclipse.jgit.lib._ 34 | import org.eclipse.jgit.revwalk._ 35 | import org.eclipse.jgit.treewalk.TreeWalk 36 | import org.eclipse.jgit.treewalk.filter.{AndTreeFilter, TreeFilter} 37 | import org.eclipse.jgit.util.FS 38 | 39 | 40 | 41 | 42 | package object git { 43 | 44 | implicit class RichByteArray(bytes: Array[Byte]) { 45 | lazy val blobId = (new Formatter).idFor(OBJ_BLOB, bytes) 46 | } 47 | 48 | def storeBlob(bytes: Array[Byte])(implicit i: ObjectInserter): ObjectId = i.insert(OBJ_BLOB, bytes) 49 | 50 | def storeBlob(text: String)(implicit i: ObjectInserter, charset: Charset): ObjectId = storeBlob(text.getBytes(charset)) 51 | 52 | def walk(trees: RevTree*)( 53 | filter: TreeFilter, 54 | recursive: Boolean = true, 55 | postOrderTraversal: Boolean = false)(implicit reader: ObjectReader) = { 56 | 57 | val tw = new TreeWalk(reader) 58 | tw.setRecursive(recursive) 59 | tw.setPostOrderTraversal(postOrderTraversal) 60 | tw.reset 61 | 62 | for (t <- trees) { 63 | tw.addTree(t) 64 | } 65 | 66 | tw.setFilter(filter) 67 | 68 | tw 69 | } 70 | 71 | implicit class RichObjectDatabase(objectDatabase: ObjectDatabase) { 72 | 73 | lazy val threadLocalResources = new ThreadLocalObjectDatabaseResources(objectDatabase) 74 | 75 | } 76 | 77 | implicit class RichObjectDirectory(objectDirectory: ObjectDirectory) { 78 | 79 | def packedObjects: Iterable[ObjectId] = 80 | for { pack <- objectDirectory.getPacks.asScala ; entry <- pack.asScala } yield entry.toObjectId 81 | 82 | } 83 | 84 | implicit class RichRepo(repo: Repository) { 85 | lazy val git = new Git(repo) 86 | 87 | lazy val topDirectory = if (repo.isBare) repo.getDirectory else repo.getWorkTree 88 | 89 | def singleThreadedReaderTuple: (RevWalk, ObjectReader) = { 90 | val revWalk=new RevWalk(repo) 91 | (revWalk, revWalk.getObjectReader) 92 | } 93 | 94 | def nonSymbolicRefs = repo.getRefDatabase.getRefs.asScala.filterNot(_.isSymbolic).toSeq 95 | } 96 | 97 | implicit class RichRefDatabase(refDatabase: RefDatabase) { 98 | val branchRefs: Seq[Ref] = refDatabase.getRefsByPrefix(R_HEADS).asScala.toSeq 99 | } 100 | 101 | implicit class RichString(str: String) { 102 | def asObjectId = jgit.lib.ObjectId.fromString(str) 103 | } 104 | 105 | implicit class RichRevTree(revTree: RevTree) { 106 | def walk(postOrderTraversal: Boolean = false)(implicit reader: ObjectReader) = { 107 | val treeWalk = new TreeWalk(reader) 108 | treeWalk.setRecursive(true) 109 | treeWalk.setPostOrderTraversal(postOrderTraversal) 110 | treeWalk.addTree(revTree) 111 | treeWalk 112 | } 113 | } 114 | 115 | implicit def treeWalkPredicateToTreeFilter(p: TreeWalk => Boolean): TreeFilter = new TreeFilter() { 116 | def include(walker: TreeWalk) = p(walker) 117 | 118 | def shouldBeRecursive() = true 119 | 120 | override def clone() = this 121 | } 122 | 123 | implicit class RichTreeWalk(treeWalk: TreeWalk) { 124 | 125 | /** 126 | * @param f - note that the function must completely extract all 127 | * information from the TreeWalk at the point of 128 | * execution, the state of TreeWalk will be altered after 129 | * execution. 130 | */ 131 | def map[V](f: TreeWalk => V): Iterator[V] = new Iterator[V] { 132 | var _hasNext = treeWalk.next() 133 | 134 | def hasNext = _hasNext 135 | 136 | def next() = { 137 | val v = f(treeWalk) 138 | _hasNext = treeWalk.next() 139 | v 140 | } 141 | } 142 | // def flatMap[B](f: TreeWalk => Iterator[B]): C[B] 143 | 144 | def withFilter(p: TreeWalk => Boolean): TreeWalk = { 145 | treeWalk.setFilter(AndTreeFilter.create(treeWalk.getFilter, p)) 146 | treeWalk 147 | } 148 | 149 | 150 | def foreach[U](f: TreeWalk => U): Unit = { 151 | while (treeWalk.next()) { 152 | f(treeWalk) 153 | } 154 | } 155 | 156 | def exists(p: TreeWalk => Boolean): Boolean = { 157 | var res = false 158 | while (!res && treeWalk.next()) res = p(treeWalk) 159 | res 160 | } 161 | 162 | def slashPrefixedPath = "/" + treeWalk.getPathString 163 | } 164 | 165 | implicit class RichRef(ref: Ref) { 166 | def targetObjectId(implicit refDatabase: RefDatabase): ObjectId = { 167 | val peeledRef = refDatabase.peel(ref) 168 | Option(peeledRef.getPeeledObjectId).getOrElse(peeledRef.getObjectId) 169 | } 170 | } 171 | 172 | implicit class RichRevObject(revObject: RevObject) { 173 | lazy val typeString = Constants.typeString(revObject.getType) 174 | 175 | def toTree(implicit revWalk: RevWalk): Option[RevTree] = treeOrBlobPointedToBy(revObject).toOption 176 | } 177 | 178 | val FileModeNames = Map( 179 | FileMode.EXECUTABLE_FILE -> "executable", 180 | FileMode.REGULAR_FILE -> "regular-file", 181 | FileMode.SYMLINK -> "symlink", 182 | FileMode.TREE -> "tree", 183 | FileMode.MISSING -> "missing", 184 | FileMode.GITLINK -> "submodule" 185 | ) 186 | 187 | implicit class RichFileMode(fileMode: FileMode) { 188 | lazy val name = FileModeNames(fileMode) 189 | } 190 | 191 | implicit class RichDiffEntry(diffEntry: DiffEntry) { 192 | import DiffEntry.Side 193 | import Side.{NEW, OLD} 194 | 195 | def isDiffableType(side: Side) = 196 | // diffEntry.getMode(side) != FileMode.GITLINK && 197 | diffEntry.getId(side) != null && diffEntry.getMode(side).getObjectType == OBJ_BLOB 198 | 199 | lazy val bothSidesDiffableType: Boolean = Side.values().map(isDiffableType).forall(d => d) 200 | 201 | def editList(implicit objectReader: ObjectReader): Option[EditList] = { 202 | def rawText(side: Side) = { 203 | objectReader.resolveExistingUniqueId(diffEntry.getId(side)).map(_.open).toOption.filterNot(_.isLarge).flatMap { 204 | l => 205 | val bytes = l.getCachedBytes 206 | if (RawText.isBinary(bytes)) None else Some(new RawText(bytes)) 207 | } 208 | } 209 | 210 | if (bothSidesDiffableType) { 211 | for (oldText <- rawText(OLD) ; newText <- rawText(NEW)) yield { 212 | val algo = DiffAlgorithm.getAlgorithm(SupportedAlgorithm.HISTOGRAM) 213 | val comp = RawTextComparator.DEFAULT 214 | algo.diff(comp, oldText, newText) 215 | } 216 | } else None 217 | } 218 | } 219 | 220 | implicit class RichObjectId(objectId: AnyObjectId) { 221 | def open(implicit objectReader: ObjectReader): ObjectLoader = objectReader.open(objectId) 222 | 223 | def sizeOpt(implicit objectReader: ObjectReader): Option[Long] = sizeTry.toOption 224 | 225 | def sizeTry(implicit objectReader: ObjectReader): Try[Long] = 226 | Try(objectReader.getObjectSize(objectId, OBJ_ANY)) 227 | 228 | def asRevObject(implicit revWalk: RevWalk) = revWalk.parseAny(objectId) 229 | 230 | def asRevCommit(implicit revWalk: RevWalk) = revWalk.parseCommit(objectId) 231 | 232 | def asRevCommitOpt(implicit revWalk: RevWalk): Option[RevCommit] = 233 | if (revWalk.getObjectReader.has(objectId)) Some(objectId.asRevCommit) else None 234 | 235 | def asRevTag(implicit revWalk: RevWalk) = revWalk.parseTag(objectId) 236 | 237 | def asRevTree(implicit revWalk: RevWalk) = revWalk.parseTree(objectId) 238 | 239 | lazy val shortName = objectId.getName.take(8) 240 | } 241 | 242 | implicit class RichObjectReader(reader: ObjectReader) { 243 | def resolveUniquely(id: AbbreviatedObjectId): Try[ObjectId] = Try(reader.resolve(id).asScala.toList).flatMap { 244 | _ match { 245 | case fullId :: Nil => Success(fullId) 246 | case ids => val resolution = if (ids.isEmpty) "no Git object" else s"${ids.size} objects : ${ids.map(reader.abbreviate).map(_.name).mkString(",")}" 247 | throw new IllegalArgumentException(s"Abbreviated id '${id.name}' resolves to $resolution") 248 | } 249 | } 250 | 251 | def resolveExistingUniqueId(id: AbbreviatedObjectId) = resolveUniquely(id).flatMap { 252 | fullId => if (reader.has(fullId)) Success(fullId) else throw new IllegalArgumentException(s"Id '$id' not found in repo") 253 | } 254 | } 255 | 256 | def abbrId(str: String)(implicit reader: ObjectReader): ObjectId = reader.resolveExistingUniqueId(AbbreviatedObjectId.fromString(str)).get 257 | 258 | def resolveGitDirFor(folder: File) = Option(RepositoryCache.FileKey.resolve(folder, FS.detect)).filter(_.exists()) 259 | 260 | def treeOrBlobPointedToBy(revObject: RevObject)(implicit revWalk: RevWalk): Either[RevBlob, RevTree] = revObject match { 261 | case commit: RevCommit => Right(commit.getTree) 262 | case tree: RevTree => Right(tree) 263 | case blob: RevBlob => Left(blob) 264 | case tag: RevTag => treeOrBlobPointedToBy(revWalk.peel(tag)) 265 | } 266 | 267 | def diff(trees: RevTree*)(implicit reader: ObjectReader): Seq[DiffEntry] = 268 | DiffEntry.scan(walk(trees: _*)(TreeFilter.ANY_DIFF)).asScala.toSeq 269 | 270 | def allBlobsUnder(tree: RevTree)(implicit reader: ObjectReader): Set[ObjectId] = 271 | tree.walk().map(_.getObjectId(0)).toSet 272 | 273 | // use ObjectWalk instead ?? 274 | def allBlobsReachableFrom(revisions: Set[String])(implicit repo: Repository): Set[ObjectId] = { 275 | implicit val (revWalk: RevWalk, reader: ObjectReader) = repo.singleThreadedReaderTuple 276 | 277 | revisions.map(repo.resolve).map { 278 | objectId => allBlobsReachableFrom(objectId.asRevObject) 279 | } reduce (_ ++ _) 280 | } 281 | 282 | @tailrec 283 | def allBlobsReachableFrom(revObject: RevObject)(implicit reader: ObjectReader): Set[ObjectId] = revObject match { 284 | case commit: RevCommit => allBlobsUnder(commit.getTree) 285 | case tree: RevTree => allBlobsUnder(tree) 286 | case blob: RevBlob => Set(blob) 287 | case tag: RevTag => allBlobsReachableFrom(tag.getObject) 288 | } 289 | 290 | } 291 | --------------------------------------------------------------------------------