Skip to content

Commit 81af761

Browse files
authored
Merge pull request #61 from elementsinteractive/archive
refactor(#52): download repository archive instead of using git clone for GitLab
2 parents 535cf6f + 2948452 commit 81af761

File tree

12 files changed

+148
-22
lines changed

12 files changed

+148
-22
lines changed

.DS_Store

6 KB
Binary file not shown.

.github/workflows/lgtm.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
run: |
4545
docker run --rm elementsinteractive/lgtm-ai \
4646
review \
47-
--pr-url "https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number }}" \
4847
--git-api-key "${{ secrets.GITHUB_TOKEN }}" \
4948
--ai-api-key "${{ secrets.AI_API_TOKEN }}" \
50-
-vv
49+
-vv \
50+
"https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number }}"

archive.tar.gz

88.8 MB
Binary file not shown.

internal/patrol/patrol.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func (s *sheriffService) scanProject(project repository.Project) (report *scanne
198198

199199
// Clone the project
200200
log.Info().Str("project", project.Path).Str("dir", dir).Str("url", project.RepoUrl).Msg("Cloning project")
201-
if err := s.repoService.Provide(project.Repository).Clone(project.RepoUrl, dir); err != nil {
201+
if err := s.repoService.Provide(project.Repository).Clone(project, dir); err != nil {
202202
return nil, errors.Join(fmt.Errorf("failed to clone project %v", project.Path), err)
203203
}
204204

internal/patrol/patrol_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ func (c *mockClient) OpenVulnerabilityIssue(project repository.Project, report s
239239
return args.Get(0).(*repository.Issue), args.Error(1)
240240
}
241241

242-
func (c *mockClient) Clone(url string, dir string) error {
243-
args := c.Called(url, dir)
242+
func (c *mockClient) Clone(project repository.Project, dir string) error {
243+
args := c.Called(project.RepoUrl, dir)
244244
return args.Error(0)
245245
}
246246

internal/publish/to_issue_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func (c *mockGitlabService) OpenVulnerabilityIssue(project repository.Project, r
242242
return args.Get(0).(*repository.Issue), args.Error(1)
243243
}
244244

245-
func (c *mockGitlabService) Clone(url string, dir string) error {
246-
args := c.Called(url, dir)
245+
func (c *mockGitlabService) Clone(project repository.Project, dir string) error {
246+
args := c.Called(project.RepoUrl, dir)
247247
return args.Error(0)
248248
}

internal/repository/github/github.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ func (s githubService) OpenVulnerabilityIssue(project repository.Project, report
6767
return nil, errors.New("OpenVulnerabilityIssue not yet implemented") // TODO #9 Add github support
6868
}
6969

70-
func (s githubService) Clone(url string, dir string) (err error) {
70+
func (s githubService) Clone(project repository.Project, dir string) (err error) {
7171
_, err = git.PlainClone(dir, false, &git.CloneOptions{
72-
URL: url,
72+
URL: project.RepoUrl,
7373
Auth: &http.BasicAuth{
7474
Username: "N/A",
7575
Password: s.token,

internal/repository/gitlab/gitlab.go

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package gitlab
22

33
import (
4+
"archive/tar"
5+
"bytes"
6+
"compress/gzip"
47
"errors"
58
"fmt"
9+
"io"
10+
"os"
11+
"path/filepath"
612
"sheriff/internal/repository"
13+
"strings"
714
"sync"
815

916
"github.com/elliotchance/pie/v2"
10-
"github.com/go-git/go-git/v5"
11-
"github.com/go-git/go-git/v5/plumbing/transport/http"
1217
"github.com/rs/zerolog/log"
1318
gitlab "gitlab.com/gitlab-org/api/client-go"
1419
)
@@ -115,17 +120,19 @@ func (s gitlabService) OpenVulnerabilityIssue(project repository.Project, report
115120
return
116121
}
117122

118-
func (s gitlabService) Clone(url string, dir string) (err error) {
119-
_, err = git.PlainClone(dir, false, &git.CloneOptions{
120-
URL: url,
121-
Auth: &http.BasicAuth{
122-
Username: "N/A",
123-
Password: s.token,
124-
},
125-
Depth: 1,
126-
})
123+
func (s gitlabService) Clone(project repository.Project, dir string) (err error) {
124+
archiveData, _, err := s.client.Archive(project.ID, &gitlab.ArchiveOptions{})
125+
if err != nil {
126+
return fmt.Errorf("failed to download archive: %w", err)
127+
}
128+
129+
// Create directory if it doesn't exist
130+
if err := os.MkdirAll(dir, 0755); err != nil {
131+
return fmt.Errorf("failed to create directory: %w", err)
132+
}
127133

128-
return err
134+
// Extract archive to directory
135+
return s.extractTarGz(bytes.NewReader(archiveData), dir)
129136
}
130137

131138
// This function receives a list of paths which can be gitlab projects or groups
@@ -344,3 +351,65 @@ func mapIssuePtr(i *gitlab.Issue) *repository.Issue {
344351

345352
return &issue
346353
}
354+
355+
// extractTarGz extracts a tar.gz archive
356+
func (s gitlabService) extractTarGz(reader io.Reader, destDir string) error {
357+
// TODO(#52): Move to a separate package when implementing GitHub "clone" through downloading archives
358+
gzReader, err := gzip.NewReader(reader)
359+
if err != nil {
360+
return fmt.Errorf("failed to create gzip reader: %w", err)
361+
}
362+
defer gzReader.Close()
363+
364+
tarReader := tar.NewReader(gzReader)
365+
366+
for {
367+
header, err := tarReader.Next()
368+
if err == io.EOF {
369+
break
370+
}
371+
if err != nil {
372+
return fmt.Errorf("failed to read tar header: %w", err)
373+
}
374+
375+
// Skip the root directory (GitLab archives have a root folder)
376+
pathParts := strings.Split(header.Name, "/")
377+
if len(pathParts) <= 1 {
378+
continue
379+
}
380+
381+
// Remove the first directory component (the root folder)
382+
relativePath := strings.Join(pathParts[1:], "/")
383+
if relativePath == "" {
384+
continue
385+
}
386+
387+
targetPath := filepath.Join(destDir, relativePath)
388+
if !strings.HasPrefix(targetPath, filepath.Clean(destDir)+string(os.PathSeparator)) {
389+
return fmt.Errorf("content of tar file is trying to write outside of destination directory: %s", relativePath)
390+
}
391+
392+
switch header.Typeflag {
393+
case tar.TypeDir:
394+
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
395+
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
396+
}
397+
case tar.TypeReg:
398+
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
399+
return fmt.Errorf("failed to create parent directory for %s: %w", targetPath, err)
400+
}
401+
402+
file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
403+
if err != nil {
404+
return fmt.Errorf("failed to create file %s: %w", targetPath, err)
405+
}
406+
defer file.Close()
407+
408+
if _, err := io.Copy(file, tarReader); err != nil {
409+
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
410+
}
411+
}
412+
}
413+
414+
return nil
415+
}

internal/repository/gitlab/gitlab_client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type iclient interface {
1515
ListProjectIssues(projectId interface{}, opt *gitlab.ListProjectIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error)
1616
CreateIssue(projectId interface{}, opt *gitlab.CreateIssueOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Issue, *gitlab.Response, error)
1717
UpdateIssue(projectId interface{}, issueId int, opt *gitlab.UpdateIssueOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Issue, *gitlab.Response, error)
18+
Archive(pid interface{}, opt *gitlab.ArchiveOptions, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)
1819
}
1920

2021
type client struct {
@@ -40,3 +41,7 @@ func (c *client) CreateIssue(projectId interface{}, opt *gitlab.CreateIssueOptio
4041
func (c *client) UpdateIssue(projectId interface{}, issueId int, opt *gitlab.UpdateIssueOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Issue, *gitlab.Response, error) {
4142
return c.client.Issues.UpdateIssue(projectId, issueId, opt, options...)
4243
}
44+
45+
func (c *client) Archive(pid interface{}, opt *gitlab.ArchiveOptions, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error) {
46+
return c.client.Repositories.Archive(pid, opt, options...)
47+
}

internal/repository/gitlab/gitlab_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package gitlab
22

33
import (
44
"errors"
5+
"os"
6+
"path/filepath"
57
"sheriff/internal/repository"
68
"testing"
79

810
"github.com/stretchr/testify/assert"
911
"github.com/stretchr/testify/mock"
12+
"github.com/stretchr/testify/require"
1013
gitlab "gitlab.com/gitlab-org/api/client-go"
1114
)
1215

@@ -199,6 +202,46 @@ func TestDereferenceProjectsPointers(t *testing.T) {
199202
assert.Equal(t, 2, dereferencedProjects[1].ID)
200203
assert.Equal(t, 2, errCount)
201204
}
205+
func TestCloneCompleteFlow(t *testing.T) {
206+
// Create temporary directory for testing
207+
tempDir, err := os.MkdirTemp("", "sheriff-clone-test-")
208+
require.NoError(t, err)
209+
defer os.RemoveAll(tempDir)
210+
211+
stubArchive, err := os.ReadFile("testdata/sample-archive.tar.gz")
212+
require.NoError(t, err)
213+
214+
mockClient := mockClient{}
215+
mockClient.On("Archive", 123, mock.Anything, mock.Anything).Return(stubArchive, &gitlab.Response{}, nil)
216+
217+
svc := gitlabService{client: &mockClient}
218+
219+
// Create a test project
220+
testProject := repository.Project{
221+
ID: 123,
222+
Name: "test-project",
223+
Path: "group/project",
224+
}
225+
226+
err = svc.Clone(testProject, tempDir)
227+
228+
// Verify no errors
229+
assert.NoError(t, err)
230+
mockClient.AssertExpectations(t)
231+
232+
// Verify files were extracted correctly
233+
readmeContent, err := os.ReadFile(filepath.Join(tempDir, "README.md"))
234+
assert.NoError(t, err)
235+
assert.Equal(t, "# Test Project\n\nThis is a test project for testing GitLab archive extraction.", string(readmeContent))
236+
237+
srcContent, err := os.ReadFile(filepath.Join(tempDir, "src", "main.go"))
238+
assert.NoError(t, err)
239+
assert.Equal(t, "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello from test project!\")\n}", string(srcContent))
240+
241+
// Verify directory structure
242+
_, err = os.Stat(filepath.Join(tempDir, "src"))
243+
assert.NoError(t, err, "src directory should exist")
244+
}
202245

203246
type mockClient struct {
204247
mock.Mock
@@ -248,3 +291,12 @@ func (c *mockClient) UpdateIssue(projectId interface{}, issueId int, opt *gitlab
248291
}
249292
return args.Get(0).(*gitlab.Issue), r, args.Error(2)
250293
}
294+
295+
func (c *mockClient) Archive(pid interface{}, opt *gitlab.ArchiveOptions, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error) {
296+
args := c.Called(pid, opt, options)
297+
var r *gitlab.Response
298+
if resp := args.Get(1); resp != nil {
299+
r = args.Get(1).(*gitlab.Response)
300+
}
301+
return args.Get(0).([]byte), r, args.Error(2)
302+
}

0 commit comments

Comments
 (0)