From a08c958a6362f29e3039119e5524233a90455f71 Mon Sep 17 00:00:00 2001
From: Gareth Jones <jones258@gmail.com>
Date: Sun, 12 Jun 2022 11:42:57 +1200
Subject: [PATCH] feat: support parsing commits from `Gemfile.lock` files

---
 README.md                                     |  4 +-
 .../fixtures/bundler/has-git-gem.lock         | 33 ++++++++++++++
 pkg/lockfile/parse-gemfile-lock.go            | 15 +++++++
 pkg/lockfile/parse-gemfile-lock_test.go       | 43 +++++++++++++++++++
 4 files changed, 93 insertions(+), 2 deletions(-)
 create mode 100644 pkg/lockfile/fixtures/bundler/has-git-gem.lock

diff --git a/README.md b/README.md
index 61968532..50a773dd 100644
--- a/README.md
+++ b/README.md
@@ -70,8 +70,8 @@ typically a few hours ahead of the offline databases, and supports commits;
 however it currently can produce false negatives for some ecosystems.
 
 > While the API supports commits, the detector currently has limited support for
-> extracting them - only the `composer.lock`, `package-lock.json`, `yarn.lock`,
-> & `pnpm.yaml` parsers include commit details
+> extracting them - only the `composer.lock`, `Gemfile.lock`,
+> `package-lock.json`, `yarn.lock`, & `pnpm.yaml` parsers include commit details
 
 You cannot use the API in `--offline` mode, but you can use both the offline
 databases and the API together; the detector will remove any duplicate results.
diff --git a/pkg/lockfile/fixtures/bundler/has-git-gem.lock b/pkg/lockfile/fixtures/bundler/has-git-gem.lock
new file mode 100644
index 00000000..01ab0d6b
--- /dev/null
+++ b/pkg/lockfile/fixtures/bundler/has-git-gem.lock
@@ -0,0 +1,33 @@
+GIT
+  remote: https://github.com/hanami/controller.git
+  revision: 027dbe2e56397b534e859fc283990cad1b6addd6
+  branch: action-new
+  specs:
+    hanami-controller (2.0.0.alpha1)
+      hanami-utils (~> 2.0.alpha)
+      rack (~> 2.0)
+
+GIT
+  remote: https://github.com/hanami/utils.git
+  revision: 5904fc9a70683b8749aa2861257d0c8c01eae4aa
+  branch: unstable
+  specs:
+    hanami-utils (2.0.0.alpha1)
+      concurrent-ruby (~> 1.0)
+      transproc (~> 1.0)
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    concurrent-ruby (1.1.7)
+    rack (2.2.3)
+    transproc (1.1.1)
+
+PLATFORMS
+  x86_64-linux
+
+DEPENDENCIES
+  hanami-utils!
+
+BUNDLED WITH
+   2.2.28
diff --git a/pkg/lockfile/parse-gemfile-lock.go b/pkg/lockfile/parse-gemfile-lock.go
index 90f51769..18575053 100644
--- a/pkg/lockfile/parse-gemfile-lock.go
+++ b/pkg/lockfile/parse-gemfile-lock.go
@@ -39,6 +39,9 @@ type gemfileLockfileParser struct {
 	dependencies   []PackageDetails
 	bundlerVersion string
 	rubyVersion    string
+
+	// holds the commit of the gem that is currently being parsed, if found
+	currentGemCommit string
 }
 
 func (parser *gemfileLockfileParser) addDependency(name string, version string) {
@@ -46,6 +49,7 @@ func (parser *gemfileLockfileParser) addDependency(name string, version string)
 		Name:      name,
 		Version:   version,
 		Ecosystem: BundlerEcosystem,
+		Commit:    parser.currentGemCommit,
 	})
 }
 
@@ -83,6 +87,14 @@ func (parser *gemfileLockfileParser) parseSource(line string) {
 	options := optionsRegexp.FindStringSubmatch(line)
 
 	if options != nil {
+		commit := strings.TrimPrefix(options[0], "  revision: ")
+
+		// if the prefix was removed then the gem being parsed is git based, so
+		// we store the commit to be included later
+		if commit != options[0] {
+			parser.currentGemCommit = commit
+		}
+
 		return
 	}
 
@@ -120,6 +132,9 @@ func (parser *gemfileLockfileParser) parse(contents string) {
 
 	for _, line := range lines {
 		if isSourceSection(line) {
+			// clear the stateful package details,
+			// since we're now parsing a new group
+			parser.currentGemCommit = ""
 			parser.state = parserStateSource
 			parser.parseSource(line)
 
diff --git a/pkg/lockfile/parse-gemfile-lock_test.go b/pkg/lockfile/parse-gemfile-lock_test.go
index 20b16395..e9a6a37b 100644
--- a/pkg/lockfile/parse-gemfile-lock_test.go
+++ b/pkg/lockfile/parse-gemfile-lock_test.go
@@ -619,3 +619,46 @@ func TestParseGemfileLock_HasLocalGem(t *testing.T) {
 		},
 	})
 }
+
+func TestParseGemfileLock_HasGitGem(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseGemfileLock("fixtures/bundler/has-git-gem.lock")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "hanami-controller",
+			Version:   "2.0.0.alpha1",
+			Ecosystem: lockfile.BundlerEcosystem,
+			Commit:    "027dbe2e56397b534e859fc283990cad1b6addd6",
+		},
+		{
+			Name:      "hanami-utils",
+			Version:   "2.0.0.alpha1",
+			Ecosystem: lockfile.BundlerEcosystem,
+			Commit:    "5904fc9a70683b8749aa2861257d0c8c01eae4aa",
+		},
+		{
+			Name:      "concurrent-ruby",
+			Version:   "1.1.7",
+			Ecosystem: lockfile.BundlerEcosystem,
+			Commit:    "",
+		},
+		{
+			Name:      "rack",
+			Version:   "2.2.3",
+			Ecosystem: lockfile.BundlerEcosystem,
+			Commit:    "",
+		},
+		{
+			Name:      "transproc",
+			Version:   "1.1.1",
+			Ecosystem: lockfile.BundlerEcosystem,
+			Commit:    "",
+		},
+	})
+}