├── .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 | [](https://index.scala-lang.org/rtyley/scala-git/scala-git/)
7 | [](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 |
4 |
5 |
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 |
4 |
5 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------