Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ jobs:
run: |
git diff --exit-code --stat -- . ':(exclude)web/package.json' ':(exclude)web/package-lock.json' ':(exclude)go.mod' ':(exclude)go.sum'

- name: Install subversion
run: sudo apt-get update && sudo apt-get install -y subversion

- name: Run tests
run: task test

Expand Down
30 changes: 30 additions & 0 deletions db/Repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/semaphoreui/semaphore/pkg/common_errors"
"github.com/semaphoreui/semaphore/pkg/git"
"github.com/semaphoreui/semaphore/pkg/svn"
"github.com/semaphoreui/semaphore/util"

log "github.com/sirupsen/logrus"
Expand All @@ -24,6 +25,11 @@ const (
RepositoryHTTP RepositoryType = "https"
RepositoryFile RepositoryType = "file"
RepositoryLocal RepositoryType = "local"

// RepositorySVN and RepositorySVNSSH are Subversion repositories, served by
// svnserve directly or tunnelled through ssh.
RepositorySVN RepositoryType = "svn"
RepositorySVNSSH RepositoryType = "svn+ssh"
)

// Repository is the model for code stored in a git repository
Expand Down Expand Up @@ -214,6 +220,10 @@ func (r Repository) GetType() RepositoryType {
return RepositoryLocal
}

if svn.IsSvnSSHURL(r.GitURL) {
return RepositorySVNSSH
}

re := regexp.MustCompile(`^(\w+)://`)
m := re.FindStringSubmatch(r.GitURL)
if m == nil {
Expand Down Expand Up @@ -257,3 +267,23 @@ func (r Repository) Validate() error {

return nil
}

// ValidateCommitRef rejects a commit a task can not be checked out at: a hex
// object name for git, a revision number for Subversion.
func (r Repository) ValidateCommitRef(ref string, objectName string) error {
if r.IsSubversion() {
return svn.ValidateRevision(ref, objectName)
}
return git.ValidateCommitHash(ref, objectName)
}

// IsSubversion reports whether the repository is served by Subversion rather
// than git.
func (r Repository) IsSubversion() bool {
switch r.GetType() {
case RepositorySVN, RepositorySVNSSH:
return true
default:
return false
}
}
73 changes: 73 additions & 0 deletions db/Repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,79 @@ func TestRepository_GetType_CaseInsensitiveScheme(t *testing.T) {
}
}

func TestRepository_GetType_Subversion(t *testing.T) {
tests := []struct {
name string
gitURL string
expected RepositoryType
}{
{"svn", "svn://svn.example.com/repo/trunk", RepositorySVN},
{"uppercase SVN", "SVN://svn.example.com/repo/trunk", RepositorySVN},
{"svn+ssh", "svn+ssh://user@svn.example.com/repo/trunk", RepositorySVNSSH},
{"uppercase svn+ssh", "SVN+SSH://svn.example.com/repo/trunk", RepositorySVNSSH},
{"git+ssh is not subversion", "git+ssh://git@example.com/repo.git", RepositorySSH},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := Repository{GitURL: tt.gitURL}
assert.Equal(t, tt.expected, repo.GetType())
assert.Equal(t, tt.expected != RepositorySSH, repo.IsSubversion())
})
}
}

func TestRepository_Validate_SubversionBranchPath(t *testing.T) {
tests := []struct {
name string
branch string
wantErr bool
}{
{"trunk", "trunk", false},
{"branch", "branches/release", false},
{"tag", "tags/v1.0", false},
{"empty", "", true},
{"parent directory", "../other", true},
{"option injection", "--config-dir=/tmp", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Repository{Name: "r", GitURL: "svn://svn.example.com/repo", GitBranch: tt.branch}.Validate()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func TestRepository_ValidateCommitRef(t *testing.T) {
git := Repository{GitURL: "https://example.com/repo.git"}
svn := Repository{GitURL: "svn://svn.example.com/repo"}

tests := []struct {
name string
repo Repository
ref string
wantErr bool
}{
{"git hash", git, "a1b2c3d", false},
{"git rejects a revision", git, "42", true},
{"svn revision", svn, "42", false},
{"svn rejects a git hash", svn, "a1b2c3d", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.repo.ValidateCommitRef(tt.ref, "task")
if tt.wantErr {
assert.EqualError(t, err, "task commit hash is invalid")
} else {
assert.NoError(t, err)
}
})
}
}

func TestRepository_GetType_WindowsLocalPath(t *testing.T) {
assert.Equal(t, RepositoryLocal, Repository{GitURL: `D:\repo`}.GetType())
assert.Equal(t, RepositoryLocal, Repository{GitURL: `D:/repo`}.GetType())
Expand Down
6 changes: 4 additions & 2 deletions db/Task.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,17 @@ func (task *Task) GetWorkflowUrl() *string {
return &workflowUrl
}

func (task *Task) ValidateNewTask(template Template) error {
// ValidateNewTask checks a task before it is created. The repository is the
// template's: the commit a task may be checked out at depends on its type.
func (task *Task) ValidateNewTask(template Template, repository Repository) error {
if task.GitBranch != nil {
if err := git.ValidateGitBranch(*task.GitBranch, "task"); err != nil {
return err
}
}

if task.CommitHash != nil {
if err := git.ValidateCommitHash(*task.CommitHash, "task"); err != nil {
if err := repository.ValidateCommitRef(*task.CommitHash, "task"); err != nil {
return err
}
}
Expand Down
4 changes: 2 additions & 2 deletions db/playbook_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestTask_ValidateNewTask_AbsolutePlaybook_ReturnsError(t *testing.T) {
Playbook: "/usr/bin/evil.sh",
}

err := task.ValidateNewTask(Template{})
err := task.ValidateNewTask(Template{}, Repository{})

assert.EqualError(t, err, "task playbook must be a relative path inside the repository")
}
Expand All @@ -91,7 +91,7 @@ func TestTask_ValidateNewTask_RelativePlaybook_ReturnsNoError(t *testing.T) {
Playbook: "site.yml",
}

err := task.ValidateNewTask(Template{})
err := task.ValidateNewTask(Template{}, Repository{})

assert.NoError(t, err)
}
9 changes: 9 additions & 0 deletions db_lib/GitClientFactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ package db_lib

import "github.com/semaphoreui/semaphore/util"

// CreateDefaultGitClient returns the configured git client, which hands
// Subversion repositories over to the svn client.
func CreateDefaultGitClient(keyInstaller AccessKeyInstaller) GitClient {
return repositoryTypeClient{
git: createConfiguredGitClient(keyInstaller),
svn: CreateSvnClient(keyInstaller),
}
}

func createConfiguredGitClient(keyInstaller AccessKeyInstaller) GitClient {
switch util.Config.GitClientId {
case util.GoGitClientId:
return CreateGoGitClient(keyInstaller)
Expand Down
Loading
Loading