Skip to content

Commit

Permalink
#1 MVP has been baked.
Browse files Browse the repository at this point in the history
  • Loading branch information
petr-korobeinikov committed Jan 10, 2021
0 parents commit b6be2d9
Show file tree
Hide file tree
Showing 16 changed files with 504 additions and 0 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# scm 💪

---

## It's time 🕒 to clone ⤵️ interesting 🧐 repo faster ⏩ and cleaner 🧹

![Usage example](demo.svg)

Just type:

```shell
scm https://github.com/pkorobeinikov/scm
```

It will clone `https://github.com/pkorobeinikov/scm` into `~/Workspace/github.com/pkorobeinikov/scm`.

It's also possible to clone `hg`-repo. So command:

```shell
scm hg http://hg.nginx.org/nginx
```

will clone `scm hg http://hg.nginx.org/nginx/` into `~/Workspace/hg.nginx.org/nginx`.

## Configuration

Put this into your `.rc`-file:

```shell
export SCM_WORKSPACE_DIR="~/Projects" # defaults to ~/Workspace
export SCM_WORKSPACE_DIR_DEFAULT_PERM="0755" # defaults to 0755
```

## Building from source

```shell
go build -o ~/Bin/scm main.go
```

## Running tests

```shell
go test -cover -v ./internal
```
1 change: 1 addition & 0 deletions demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module scm

go 1.15
29 changes: 29 additions & 0 deletions internal/args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package internal

import (
"errors"
)

func ParseArgs(args []string) (scmBin, scmUrl string, err error) {
switch len(args) {
default:
err = TooLongArgumentListErr
return
case 1:
err = NotEnoughArgumentsErr
return
case 2:
scmBin = "git"
scmUrl = args[1]
return
case 3:
scmBin = args[1]
scmUrl = args[2]
return
}
}

var (
NotEnoughArgumentsErr = errors.New(`need at least one argument with repo url`)
TooLongArgumentListErr = errors.New(`too long argument list`)
)
91 changes: 91 additions & 0 deletions internal/args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package internal

import (
"fmt"
"testing"
)

func TestParseArgs(t *testing.T) {
t.Run(`complex`, func(t *testing.T) {
testCases := []struct {
name string
given []string
expected struct {
scmBin string
scmUrl string
err error
}
}{
{
name: "not enough arguments",
given: []string{"scm"},
expected: struct {
scmBin string
scmUrl string
err error
}{
scmBin: "",
scmUrl: "",
err: NotEnoughArgumentsErr,
},
},
{
name: "too long argument list",
given: []string{"foo", "bar", "baz", "quix"},
expected: struct {
scmBin string
scmUrl string
err error
}{
scmBin: "",
scmUrl: "",
err: TooLongArgumentListErr,
},
},
{
name: "git by default",
given: []string{"scm", "https://github.com/user/repo"},
expected: struct {
scmBin string
scmUrl string
err error
}{
scmBin: "git",
scmUrl: "https://github.com/user/repo",
err: nil,
},
},
{
name: "hg if needed",
given: []string{"scm", "hg", "http://hg.robustwebserver.org/robustwebserver/"},
expected: struct {
scmBin string
scmUrl string
err error
}{
scmBin: "hg",
scmUrl: "http://hg.robustwebserver.org/robustwebserver/",
err: nil,
},
},
}

for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s", testCase.name), func(t *testing.T) {
actualScmBin, actualScmUrl, actualErr := ParseArgs(testCase.given)

if testCase.expected.scmBin != actualScmBin {
t.Fail()
}

if testCase.expected.scmUrl != actualScmUrl {
t.Fail()
}

if testCase.expected.err != actualErr {
t.Fail()
}
})
}
})
}
35 changes: 35 additions & 0 deletions internal/cfg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package internal

import (
"os"
"path/filepath"
"strconv"
)

type Cfg struct {
ScmWorkspaceDirDefaultPerm os.FileMode
ScmWorkingCopyPath string
}

func ReadCfg(scmUrl string) (Cfg, error) {
scmWorkspaceDir := LookupEnvOrDefault("SCM_WORKSPACE_DIR", "~/Workspace")
scmExpanedWorkspaceDir, err := ExpandHomeDir(scmWorkspaceDir)
if err != nil {
return Cfg{}, err
}

scmWorkspaceDirDefaultPermStr := LookupEnvOrDefault("SCM_WORKSPACE_DIR_DEFAULT_PERM", "0755")
scmWorkspaceDirDefaultPerm, err := strconv.ParseInt(scmWorkspaceDirDefaultPermStr, 8, strconv.IntSize)
scmWorkspaceDirDefaultPermFileMode := os.FileMode(scmWorkspaceDirDefaultPerm)

scmPathFromUrl, err := ExtractLocalPathFromScmURL(scmUrl)
if err != nil {
return Cfg{}, err
}
scmWorkingCopyPath := filepath.Join(scmExpanedWorkspaceDir, scmPathFromUrl)

return Cfg{
ScmWorkspaceDirDefaultPerm: scmWorkspaceDirDefaultPermFileMode,
ScmWorkingCopyPath: scmWorkingCopyPath,
}, nil
}
52 changes: 52 additions & 0 deletions internal/cfg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package internal

import (
"os"
"testing"
)

func TestReadCfg(t *testing.T) {
t.Run(`positive`, func(t *testing.T) {
saveScmWorkspaceDir := os.Getenv(`SCM_WORKSPACE_DIR`)

expected := Cfg{
ScmWorkspaceDirDefaultPerm: 0755,
ScmWorkingCopyPath: `/tmp/Workspace/github.com/user/repo`,
}

_ = os.Setenv(`SCM_WORKSPACE_DIR`, `/tmp/Workspace`)
actual, _ := ReadCfg(`https://github.com/user/repo`)

if expected.ScmWorkspaceDirDefaultPerm != actual.ScmWorkspaceDirDefaultPerm {
t.Errorf(`want "%s", got "%s"`, expected.ScmWorkspaceDirDefaultPerm, actual.ScmWorkspaceDirDefaultPerm)
}

if expected.ScmWorkingCopyPath != actual.ScmWorkingCopyPath {
t.Errorf(`want "%s", got "%s"`, expected.ScmWorkingCopyPath, actual.ScmWorkingCopyPath)
}

_ = os.Setenv(`SCM_WORKSPACE_DIR`, saveScmWorkspaceDir)
})

t.Run(`homedir not detected`, func(t *testing.T) {
saveScmWorkspaceDir := os.Getenv(`SCM_WORKSPACE_DIR`)
saveHome := os.Getenv(`HOME`)

_ = os.Unsetenv(`HOME`)
_ = os.Setenv(`SCM_WORKSPACE_DIR`, `~/Workspace`)
_, err := ReadCfg(`https://github.com/user/repo`)
if err == nil {
t.Errorf(`homedir detected but shouldn't'`)
}

_ = os.Setenv(`HOME`, saveHome)
_ = os.Setenv(`SCM_WORKSPACE_DIR`, saveScmWorkspaceDir)
})

t.Run(`mailformed repo url given`, func(t *testing.T) {
_, err := ReadCfg(`https://github % com/user/repo`)
if err == nil {
t.Errorf(`repo url parsed but shouldn't'`)
}
})
}
22 changes: 22 additions & 0 deletions internal/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package internal

import (
"os"
"os/exec"
)

func PrepareLocalWorkingCopyPath(scmWorkingCopyPath string, scmWorkspaceDirDefaultPermFileMode os.FileMode) error {
return os.MkdirAll(scmWorkingCopyPath, scmWorkspaceDirDefaultPermFileMode)
}

func Clone(scmBin, scmUrl, scmWorkingCopyPath string) error {
scmCmd := exec.Command(scmBin, "clone", scmUrl, scmWorkingCopyPath)
scmCmd.Stdout = os.Stdout
scmCmd.Stderr = os.Stderr

if err := scmCmd.Run(); err != nil {
return err
}

return nil
}
35 changes: 35 additions & 0 deletions internal/clone_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package internal

import (
"os"
"testing"
)

func TestPrepareLocalWorkingCopyPath(t *testing.T) {
t.Run(`positive`, func(t *testing.T) {
err := PrepareLocalWorkingCopyPath(`/tmp/Workspace/foobar`, 0755)
if err != nil {
t.Error(`expected to create workspace directory without any errors`)
}
})
}

func TestClone(t *testing.T) {
t.Run(`positive`, func(t *testing.T) {
dest := `/tmp/github/gitignore`

err := Clone(`git`, `https://github.com/github/gitignore`, dest)
if err != nil {
t.Error(`expected to clone repo without any errors`)
}

_ = os.RemoveAll(dest)
})

t.Run(`negative`, func(t *testing.T) {
err := Clone(`unknown-scm`, `https://github.com/github/gitignore`, `/tmp/github/gitignore`)
if err == nil {
t.Error(`expected to fail with unknown command`)
}
})
}
20 changes: 20 additions & 0 deletions internal/homedir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package internal

import (
"os"
"path/filepath"
"strings"
)

func ExpandHomeDir(d string) (string, error) {
if strings.HasPrefix(d, `~/`) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}

return filepath.Join(homeDir, d[2:]), nil
}

return d, nil
}
35 changes: 35 additions & 0 deletions internal/homedir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package internal

import (
"os"
"testing"
)

func TestExpandHomeDir(t *testing.T) {
t.Run(`positive`, func(t *testing.T) {
actual, _ := ExpandHomeDir(`~/Workspace`)
// fixme hardcoded value
if `/Users/pkorobeinikov/Workspace` != actual {
t.Fail()
}
})

t.Run(`no home dir in path`, func(t *testing.T) {
actual, _ := ExpandHomeDir(`/mnt/Volumes/Workspace`)
if `/mnt/Volumes/Workspace` != actual {
t.Fail()
}
})

t.Run(`homedir not set`, func(t *testing.T) {
saveHome := os.Getenv(`HOME`)

_ = os.Unsetenv(`HOME`)
_, err := ExpandHomeDir(`~/Workspace`)
if err == nil {
t.Fail()
}

_ = os.Setenv(`HOME`, saveHome)
})
}
11 changes: 11 additions & 0 deletions internal/lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package internal

import "os"

func LookupEnvOrDefault(envName, defaultValue string) string {
if value, found := os.LookupEnv(envName); found {
return value
} else {
return defaultValue
}
}
Loading

0 comments on commit b6be2d9

Please sign in to comment.