├── fixtures ├── history │ ├── bash_history_test │ └── zsh_history_test ├── store.yaml └── list.go ├── githooks └── pre-push ├── .goreleaser.yml ├── .golangci.yml ├── .gitignore ├── .github ├── pull_request_template.md └── workflows │ ├── lint.yaml │ ├── test.yaml │ ├── generate-tag.yaml │ └── release.yaml ├── main.go ├── internal ├── exec │ ├── editor.go │ ├── git.go │ └── exec.go ├── printers │ ├── spinner.go │ └── printers.go ├── files │ └── files.go ├── history │ ├── history_test.go │ └── history.go ├── config │ └── config.go └── store │ ├── store.go │ └── store_test.go ├── cmd ├── update_test.go ├── version_test.go ├── version.go ├── add.go ├── ckp.go ├── find_test.go ├── list_internal_test.go ├── pull_test.go ├── reset.go ├── reset_test.go ├── push_test.go ├── pull.go ├── find_internal_test.go ├── rm_test.go ├── update.go ├── find.go ├── run_test.go ├── run.go ├── add_history_test.go ├── list_test.go ├── add_solution_test.go ├── init_test.go ├── init.go ├── rm.go ├── push.go ├── list.go ├── add_history.go ├── add_code_test.go ├── add_solution.go ├── edit_test.go ├── add_code.go └── edit.go ├── .golangci-errcheck-exclude.txt ├── go.mod ├── LICENSE ├── Makefile ├── Formula └── ckp.rb ├── mocks ├── IPrinters.go └── IExec.go ├── README.md └── install.sh /fixtures/history/bash_history_test: -------------------------------------------------------------------------------- 1 | exit 2 | 3 | echo "je suis con"; echo "tu es con" 4 | 5 | pwd 6 | cd 7 | -------------------------------------------------------------------------------- /githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | remote="$1" 4 | url="$2" 5 | 6 | make lint || exit 1 7 | make test || exit 1 8 | 9 | exit 0 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | brews: 2 | - tap: 3 | owner: elhmn 4 | name: ckp 5 | folder: Formula 6 | homepage: https://github.com/elhmn/ckp 7 | -------------------------------------------------------------------------------- /fixtures/history/zsh_history_test: -------------------------------------------------------------------------------- 1 | : 1602405531:0;cat /dev/null > ~/.bash_history \ 2 | 3 | : 1602405534:0;history 4 | : 1602405620:0;echo "je suis con"; echo "tu es con" 5 | 6 | : 1602406506:0;echo "je suis con end of line" 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - whitespace 5 | - govet 6 | - errcheck 7 | - deadcode 8 | - gosimple 9 | - unused 10 | - structcheck 11 | - staticcheck 12 | - typecheck 13 | - varcheck 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | codekeeper 8 | ckp 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | #vim 17 | .*.sw* 18 | .*.orig 19 | tags 20 | tags.* 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### This pull request does... 2 | 3 | **Why ?** 4 | Describe why this pull request is created and what it does 5 | 6 | **How ?** 7 | Let us know how it does what it does 8 | 9 | **Steps to verify:** 10 | Write down few steps to help us test or verify what the pull request does 11 | 12 | ** Screenshots (optional) 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/elhmn/ckp/cmd" 7 | "github.com/elhmn/ckp/internal/config" 8 | ) 9 | 10 | var version = "0.0.0+dev" 11 | 12 | func main() { 13 | conf := config.NewDefaultConfig(config.Options{Version: version}) 14 | command := cmd.NewCKPCommand(conf) 15 | 16 | err := command.Execute() 17 | if err != nil { 18 | os.Exit(1) 19 | } 20 | 21 | os.Exit(0) 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | name: Lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup 12 | uses: 13 | actions/setup-go@v2 14 | with: 15 | go-version: 1.16.9 16 | id: go 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Run lint 22 | run: make lint 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup 12 | uses: 13 | actions/setup-go@v2 14 | with: 15 | go-version: 1.16.9 16 | id: go 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Run tests 22 | run: make test 23 | -------------------------------------------------------------------------------- /internal/exec/editor.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "os" 5 | exe "os/exec" 6 | ) 7 | 8 | const defaultEditor = "vim" 9 | 10 | //OpenEditor opens the editor 11 | func (ex Exec) OpenEditor(editor string, args ...string) error { 12 | if editor == "" { 13 | editor = defaultEditor 14 | } 15 | 16 | cmd := exe.Command(editor, args...) 17 | cmd.Stdout = os.Stdout 18 | cmd.Stderr = os.Stderr 19 | cmd.Stdin = os.Stdin 20 | return cmd.Run() 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/store.yaml: -------------------------------------------------------------------------------- 1 | scripts: 2 | - id: hash-of-file-content 3 | creationtime: 2021-05-08T13:08:34.323371+02:00 4 | updatetime: 2021-05-08T13:08:34.323371+02:00 5 | comment: "a basic comment" 6 | code: 7 | alias: "an alias" 8 | content: | 9 | echo "mon code" 10 | - id: hash-of-file-content-2 11 | creationtime: 2021-05-08T13:08:34.323371+02:00 12 | updatetime: 2021-05-08T13:08:34.323371+02:00 13 | comment: "a basic second comment" 14 | solution: 15 | content: link to your hashnode or medium article 16 | -------------------------------------------------------------------------------- /cmd/update_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | ) 9 | 10 | //TestUpdateCommand test the `ckp update` command 11 | func TestUpdateCommand(t *testing.T) { 12 | t.Run("Run the update successfully", func(t *testing.T) { 13 | conf := createConfig(t) 14 | writer := &bytes.Buffer{} 15 | conf.OutWriter = writer 16 | 17 | command := cmd.NewUpdateCommand(conf) 18 | 19 | //Set writer 20 | command.SetOutput(conf.OutWriter) 21 | 22 | err := command.Execute() 23 | if err != nil { 24 | t.Errorf("Error: failed with %s", err) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /.golangci-errcheck-exclude.txt: -------------------------------------------------------------------------------- 1 | //This file specify which functions or method should be excluded 2 | //from golangci errcheck linter 3 | // check this page for more information https://github.com/kisielk/errcheck#excluding-functions 4 | 5 | //the function exclusion does not seem to work with golangci 6 | //as it needs to be updated https://github.com/golangci/golangci-lint/issues/959 7 | 8 | (*net/http.ResponseWriter).Write 9 | (net/http.ResponseWriter).Write 10 | 11 | (*github.com/fatih/color.Color).Printf 12 | (*github.com/fatih/color.Color).Println 13 | (*github.com/fatih/color.Color).Fprintf 14 | 15 | fmt.Printf 16 | fmt.Println 17 | fmt.Print 18 | fmt.Fprintf 19 | fmt.Fprintln 20 | fmt.Fprint 21 | fmt.Scan 22 | -------------------------------------------------------------------------------- /internal/exec/git.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | //DoGit execute a `git command ` 4 | func (ex Exec) DoGit(dir string, args ...string) (string, error) { 5 | output, err := ex.Run(dir, "git", args...) 6 | return string(output), err 7 | } 8 | 9 | //DoGitClone execute a `git clone ` 10 | func (ex Exec) DoGitClone(dir string, args ...string) (string, error) { 11 | cmd := "clone" 12 | args = append([]string{cmd}, args...) 13 | output, err := ex.DoGit(dir, args...) 14 | return string(output), err 15 | } 16 | 17 | //DoGitPush execute a `git push ` 18 | func (ex Exec) DoGitPush(dir string, args ...string) (string, error) { 19 | cmd := "push" 20 | args = append([]string{cmd}, args...) 21 | output, err := ex.DoGit(dir, args...) 22 | return string(output), err 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/generate-tag.yaml: -------------------------------------------------------------------------------- 1 | name: Generate tags 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | generate-tag-from-semver: 10 | if: "!contains(github.event.head_commit.author.name, 'goreleaserbot')" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} 17 | fetch-depth: 0 18 | 19 | - name: Install svu 20 | run: | 21 | echo 'deb [trusted=yes] https://apt.fury.io/caarlos0/ /' | sudo tee /etc/apt/sources.list.d/caarlos0.list 22 | sudo apt update 23 | sudo apt install svu 24 | 25 | - name: Create next tag 26 | run: | 27 | tagname=$(svu next); git tag $tagname && git push origin $tagname 28 | -------------------------------------------------------------------------------- /cmd/version_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | //TestVersionCommand test the `ckp version` command 12 | func TestVersionCommand(t *testing.T) { 13 | t.Run("showed version successfully", func(t *testing.T) { 14 | conf := createConfig(t) 15 | writer := &bytes.Buffer{} 16 | conf.OutWriter = writer 17 | 18 | command := cmd.NewVersionCommand(conf) 19 | 20 | //Set writer 21 | command.SetOutput(conf.OutWriter) 22 | 23 | err := command.Execute() 24 | if err != nil { 25 | t.Errorf("Error: failed with %s", err) 26 | } 27 | 28 | got := writer.String() 29 | exp := "Version: 0.0.0+dev\nBuild by elhmn\nSupport osscameroon here https://opencollective.com/osscameroon\n" 30 | assert.Contains(t, got, exp) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/elhmn/ckp/internal/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | //This version will be set by a goreleaser ldflag 11 | var versionString = `Version: %s 12 | Build by elhmn 13 | Support osscameroon here https://opencollective.com/osscameroon 14 | ` 15 | 16 | //NewVersionCommand output program version 17 | func NewVersionCommand(conf config.Config) *cobra.Command { 18 | command := &cobra.Command{ 19 | Use: "version", 20 | Short: "Show ckp version", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := versionCommand(conf); err != nil { 23 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 24 | return 25 | } 26 | }, 27 | } 28 | 29 | return command 30 | } 31 | 32 | func versionCommand(conf config.Config) error { 33 | fmt.Fprintf(conf.OutWriter, versionString, conf.Version) 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/printers/spinner.go: -------------------------------------------------------------------------------- 1 | package printers 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/briandowns/spinner" 8 | ) 9 | 10 | //ISpinner defines a spinner interface 11 | type ISpinner interface { 12 | Message(m string) 13 | Clear() 14 | Stop() 15 | Start() 16 | } 17 | 18 | //Spinner is a spinner object 19 | type Spinner struct { 20 | s *spinner.Spinner 21 | } 22 | 23 | //NewSpinner returns a new Spinner 24 | func NewSpinner() *Spinner { 25 | spin := Spinner{s: spinner.New(spinner.CharSets[11], 100*time.Millisecond)} 26 | return &spin 27 | } 28 | 29 | //Message will set the spinner suffix message 30 | func (s Spinner) Message(m string) { 31 | s.s.Suffix = fmt.Sprintf(" %s", m) 32 | } 33 | 34 | //Clear will clear the spinner suffix 35 | func (s Spinner) Clear() { 36 | s.s.Suffix = "" 37 | } 38 | 39 | //Stop stops the spinner 40 | func (s Spinner) Stop() { 41 | s.s.Stop() 42 | } 43 | 44 | //Start starts the spinner 45 | func (s Spinner) Start() { 46 | s.s.Start() 47 | } 48 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/elhmn/ckp/internal/config" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | //NewAddCommand adds everything that written after --code or --solution flag 9 | func NewAddCommand(conf config.Config) *cobra.Command { 10 | command := &cobra.Command{ 11 | Use: "add", 12 | Short: "will store your code snippets or solutions", 13 | Long: `will store your code snippets or solutions 14 | 15 | example: ckp add code 'echo je suis con' 16 | Will store 'echo je suis con' as a code asset in your solution repository 17 | 18 | 19 | example: ckp add solution 'https://opensource.code' 20 | Will store 'https://opensource.code' as a solution asset in your solution repository 21 | `, 22 | } 23 | 24 | //Add commands 25 | command.AddCommand(NewAddCodeCommand(conf)) 26 | command.AddCommand(NewAddSolutionCommand(conf)) 27 | command.AddCommand(NewAddHistoryCommand(conf)) 28 | 29 | //Set flags 30 | command.PersistentFlags().StringP("comment", "m", "", `ckp add -m `) 31 | return command 32 | } 33 | -------------------------------------------------------------------------------- /cmd/ckp.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/elhmn/ckp/internal/config" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | //NewCKPCommand returns a new ckp cobra command 9 | func NewCKPCommand(config config.Config) *cobra.Command { 10 | var ckpCommand = &cobra.Command{ 11 | Use: "ckp", 12 | Short: "ckp saves and pull your bash scripts", 13 | Long: `ckp is a tool that helps you save and fetch the bash scripts you use frequently`, 14 | } 15 | 16 | ckpCommand.AddCommand(NewInitCommand(config)) 17 | ckpCommand.AddCommand(NewResetCommand(config)) 18 | ckpCommand.AddCommand(NewAddCommand(config)) 19 | ckpCommand.AddCommand(NewListCommand(config)) 20 | ckpCommand.AddCommand(NewFindCommand(config)) 21 | ckpCommand.AddCommand(NewPushCommand(config)) 22 | ckpCommand.AddCommand(NewPullCommand(config)) 23 | ckpCommand.AddCommand(NewRmCommand(config)) 24 | ckpCommand.AddCommand(NewEditCommand(config)) 25 | ckpCommand.AddCommand(NewRunCommand(config)) 26 | ckpCommand.AddCommand(NewVersionCommand(config)) 27 | ckpCommand.AddCommand(NewUpdateCommand(config)) 28 | return ckpCommand 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elhmn/ckp 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/blang/semver v3.5.1+incompatible 7 | github.com/briandowns/spinner v1.16.0 8 | github.com/fatih/color v1.13.0 // indirect 9 | github.com/golang/mock v1.6.0 10 | github.com/google/go-querystring v1.1.0 // indirect 11 | github.com/manifoldco/promptui v0.9.0 12 | github.com/mattn/go-colorable v0.1.11 // indirect 13 | github.com/mitchellh/go-homedir v1.1.0 14 | github.com/rhysd/go-github-selfupdate v1.2.3 15 | github.com/spf13/cobra v1.2.1 16 | github.com/spf13/pflag v1.0.5 17 | github.com/spf13/viper v1.9.0 18 | github.com/stretchr/testify v1.7.0 19 | github.com/ulikunitz/xz v0.5.10 // indirect 20 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect 21 | golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 // indirect 22 | golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 // indirect 23 | golang.org/x/sys v0.0.0-20211102192858-4dd72447c267 // indirect 24 | golang.org/x/text v0.3.7 // indirect 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 26 | gopkg.in/yaml.v2 v2.4.0 27 | ) 28 | -------------------------------------------------------------------------------- /cmd/find_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/internal/store" 9 | "github.com/elhmn/ckp/mocks" 10 | "github.com/golang/mock/gomock" 11 | ) 12 | 13 | func TestFindComment(t *testing.T) { 14 | t.Run("make sure that is actually implemented", func(t *testing.T) { 15 | conf := createConfig(t) 16 | mockedPrinters := conf.Printers.(*mocks.MockIPrinters) 17 | 18 | //Specify expectations 19 | mockedPrinters.EXPECT().SelectScriptEntry(gomock.Any(), store.EntryTypeAll) 20 | 21 | //setup temporary folder 22 | if err := setupFolder(conf); err != nil { 23 | t.Errorf("Error: failed with %s", err) 24 | } 25 | 26 | writer := &bytes.Buffer{} 27 | conf.OutWriter = writer 28 | command := cmd.NewFindCommand(conf) 29 | 30 | //Set writer 31 | command.SetOutput(conf.OutWriter) 32 | 33 | err := command.Execute() 34 | if err != nil { 35 | t.Errorf("Error: failed with %s", err) 36 | } 37 | 38 | if err := deleteFolder(conf); err != nil { 39 | t.Errorf("Error: failed with %s", err) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Boris Mbarga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/list_internal_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/elhmn/ckp/fixtures" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestListScripts(t *testing.T) { 11 | list := fixtures.GetListWithMoreThan10Elements() 12 | 13 | t.Run("Returns 11 elements", func(t *testing.T) { 14 | got := listScripts(list, false, false, false, len(list)) 15 | exp := fixtures.GetPrintListWithMoreThan10Elements() 16 | assert.Equal(t, exp, got) 17 | }) 18 | 19 | t.Run("Returns 2 elements with limit of 2", func(t *testing.T) { 20 | got := listScripts(list, false, false, false, 2) 21 | exp := fixtures.GetPrintListWithLessThan2Elements() 22 | assert.Equal(t, exp, got) 23 | }) 24 | 25 | t.Run("Returns only elements of type code", func(t *testing.T) { 26 | got := listScripts(list, true, false, false, len(list)) 27 | exp := fixtures.GetPrintListOnlyCode() 28 | assert.Equal(t, exp, got) 29 | }) 30 | 31 | t.Run("Returns only elements of type solution", func(t *testing.T) { 32 | got := listScripts(list, false, true, false, len(list)) 33 | exp := fixtures.GetPrintListOnlySolution() 34 | assert.Equal(t, exp, got) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /internal/files/files.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | ) 10 | 11 | //CopyFileToHomeDirectory copy the file at `filepath` to the home directory 12 | func CopyFileToHomeDirectory(filepath, contentPath string) error { 13 | content, err := ioutil.ReadFile(contentPath) 14 | if err != nil { 15 | return fmt.Errorf("failed to read file %s data: %s", contentPath, err) 16 | } 17 | 18 | home, err := homedir.Dir() 19 | if err != nil { 20 | return fmt.Errorf("failed to read home directory: %s", err) 21 | } 22 | destination := fmt.Sprintf("%s/%s", home, filepath) 23 | 24 | //Copy the store file to a temporary destination 25 | if err := ioutil.WriteFile(destination, content, 0666); err != nil { 26 | return fmt.Errorf("failed to write to file %s: %s", filepath, err) 27 | } 28 | 29 | return nil 30 | } 31 | 32 | //DeleteFileFromHomeDirectory delete the file at `filepath` from the home directory 33 | func DeleteFileFromHomeDirectory(filepath string) error { 34 | home, err := homedir.Dir() 35 | if err != nil { 36 | return fmt.Errorf("failed to read home directory: %s", err) 37 | } 38 | 39 | return os.Remove(fmt.Sprintf("%s/%s", home, filepath)) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/pull_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | //TestPullCommand test the `ckp pull` command 14 | func TestPullCommand(t *testing.T) { 15 | t.Run("pull successfully", func(t *testing.T) { 16 | conf := createConfig(t) 17 | mockedExec := conf.Exec.(*mocks.MockIExec) 18 | writer := &bytes.Buffer{} 19 | conf.OutWriter = writer 20 | 21 | //Specify expectations 22 | gomock.InOrder( 23 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 24 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any(), gomock.Any()), 25 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 26 | ) 27 | 28 | command := cmd.NewPullCommand(conf) 29 | //Set writer 30 | command.SetOutput(conf.OutWriter) 31 | 32 | err := command.Execute() 33 | if err != nil { 34 | t.Errorf("Error: failed with %s", err) 35 | } 36 | 37 | exp := "\nckp store was pulled successfully\n" 38 | got := writer.String() 39 | if !assert.Equal(t, exp, got) { 40 | t.Errorf("expected failure with [%s], got [%s]", exp, got) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/reset.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/elhmn/ckp/internal/config" 8 | "github.com/mitchellh/go-homedir" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | //NewResetCommand create new cobra command for the reset command 13 | func NewResetCommand(conf config.Config) *cobra.Command { 14 | command := &cobra.Command{ 15 | Use: "reset", 16 | Short: "removes the current remote storage repository", 17 | Long: `removes the current remote storage repository 18 | 19 | example: ckp reset 20 | `, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := resetCommand(conf); err != nil { 23 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 24 | return 25 | } 26 | }, 27 | } 28 | 29 | return command 30 | } 31 | 32 | func resetCommand(conf config.Config) error { 33 | //Setup spinner 34 | conf.Spin.Start() 35 | defer conf.Spin.Stop() 36 | 37 | home, err := homedir.Dir() 38 | if err != nil { 39 | return fmt.Errorf("failed to read home directory: %s", err) 40 | } 41 | 42 | //Delete the temporary file 43 | dir := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, conf.CKPStorageFolder) 44 | if err := os.RemoveAll(dir); err != nil { 45 | return fmt.Errorf("failed to remote storage %s: %s", dir, err) 46 | } 47 | 48 | fmt.Fprintf(conf.OutWriter, "ckp was successfully reset\n") 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APPNAME=ckp 2 | 3 | .PHONY: build help check-lint clean build lint test 4 | .DEFAULT_GOAL := help 5 | 6 | ## test: run tests on cmd and pkg files. 7 | test: vet fmt 8 | go test ./... 9 | 10 | fmt: 11 | go fmt ./... 12 | 13 | vet: 14 | go vet ./... 15 | 16 | ## build: build application binary. 17 | build: 18 | go build -o $(APPNAME) 19 | 20 | check-lint: 21 | ifeq (, $(shell which golangci-lint)) 22 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.23.8 23 | endif 24 | ifeq (, $(shell which errcheck)) 25 | go get -u github.com/kisielk/errcheck 26 | endif 27 | 28 | ## lint: run linters over the entire code base 29 | lint: check-lint 30 | golangci-lint run ./... --timeout 15m0s 31 | errcheck -exclude ./.golangci-errcheck-exclude.txt ./... 32 | 33 | ## install-hooks: install hooks 34 | install-hooks: 35 | ln -s $(PWD)/githooks/pre-push .git/hooks/pre-push 36 | 37 | ## mockgen: generate mocks 38 | mockgen: 39 | mockgen -source internal/exec/exec.go -destination mocks/IExec.go -package=mocks 40 | mockgen -source internal/printers/printers.go -destination mocks/IPrinters.go -package=mocks 41 | 42 | 43 | ## clean: remove releases 44 | clean: 45 | rm -rf $(APPNAME) 46 | 47 | all: help 48 | help: Makefile 49 | @echo " Choose a command..." 50 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 51 | -------------------------------------------------------------------------------- /cmd/reset_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/elhmn/ckp/cmd" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestResetCommand(t *testing.T) { 13 | t.Run("make sure that it runs successfully", func(t *testing.T) { 14 | conf := createConfig(t) 15 | 16 | if err := setupFolder(conf); err != nil { 17 | t.Errorf("Error: failed with %s", err) 18 | } 19 | 20 | writer := &bytes.Buffer{} 21 | conf.OutWriter = writer 22 | 23 | commandName := "reset" 24 | command := cmd.NewResetCommand(conf) 25 | 26 | //Set writer 27 | command.SetOutput(conf.OutWriter) 28 | 29 | //Set args 30 | command.SetArgs([]string{commandName}) 31 | 32 | err := command.Execute() 33 | if err != nil { 34 | t.Errorf("Error: failed with %s", err) 35 | } 36 | 37 | //Get remote storage folder 38 | folder, err := getTempStorageFolder(conf) 39 | if err != nil { 40 | t.Errorf("failed to get temporary storage folder: %s: %s", folder, err) 41 | } 42 | 43 | //Assert that the command deleted the remote storage folder 44 | if _, err := os.Stat(folder); !os.IsNotExist(err) { 45 | t.Errorf("Failed to remove %s folder : %s", folder, err) 46 | } 47 | 48 | got := writer.String() 49 | exp := "ckp was successfully reset\n" 50 | assert.Equal(t, exp, got) 51 | 52 | if err := deleteFolder(conf); err != nil { 53 | t.Errorf("Error: failed with %s", err) 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup 17 | uses: 18 | actions/setup-go@v2 19 | with: 20 | go-version: 1.16.9 21 | id: go 22 | 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Run lint 27 | run: make lint 28 | 29 | test: 30 | needs: lint 31 | name: Test 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Setup 35 | uses: 36 | actions/setup-go@v2 37 | with: 38 | go-version: 1.16.9 39 | id: go 40 | 41 | - name: Checkout 42 | uses: actions/checkout@v2 43 | 44 | - name: Run tests 45 | run: make test 46 | 47 | goreleaser: 48 | needs: test 49 | runs-on: ubuntu-latest 50 | steps: 51 | - 52 | name: Checkout 53 | uses: actions/checkout@v2 54 | with: 55 | fetch-depth: 0 56 | - 57 | name: Set up Go 58 | uses: actions/setup-go@v2 59 | with: 60 | go-version: 1.16.9 61 | - 62 | name: Run GoReleaser 63 | uses: goreleaser/goreleaser-action@v2 64 | with: 65 | version: latest 66 | args: release --rm-dist 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} 69 | -------------------------------------------------------------------------------- /Formula/ckp.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | # This file was generated by GoReleaser. DO NOT EDIT. 5 | class Ckp < Formula 6 | desc "" 7 | homepage "https://github.com/elhmn/ckp" 8 | version "0.19.0" 9 | 10 | on_macos do 11 | if Hardware::CPU.arm? 12 | url "https://github.com/elhmn/ckp/releases/download/v0.19.0/ckp_0.19.0_darwin_arm64.tar.gz" 13 | sha256 "c19a09be952b6f7ad3b6227ae1f7f5fab52308587914d0bf0bb1be35ff42cb26" 14 | 15 | def install 16 | bin.install "ckp" 17 | end 18 | end 19 | if Hardware::CPU.intel? 20 | url "https://github.com/elhmn/ckp/releases/download/v0.19.0/ckp_0.19.0_darwin_amd64.tar.gz" 21 | sha256 "506169508d593429054e9cd1a223bd57740bd9f7b1fecad54cde2d083aea76a2" 22 | 23 | def install 24 | bin.install "ckp" 25 | end 26 | end 27 | end 28 | 29 | on_linux do 30 | if Hardware::CPU.intel? 31 | url "https://github.com/elhmn/ckp/releases/download/v0.19.0/ckp_0.19.0_linux_amd64.tar.gz" 32 | sha256 "d8b107ae6f132fae74f424e770b6271af8b7e655ca4a4a95cc2ee5bfe37a7ef0" 33 | 34 | def install 35 | bin.install "ckp" 36 | end 37 | end 38 | if Hardware::CPU.arm? && Hardware::CPU.is_64_bit? 39 | url "https://github.com/elhmn/ckp/releases/download/v0.19.0/ckp_0.19.0_linux_arm64.tar.gz" 40 | sha256 "c29a70c732ea401c7137d24b7cbdf3ff2fc39fb6be782040928f596cfde369b4" 41 | 42 | def install 43 | bin.install "ckp" 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /internal/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "os" 5 | exe "os/exec" 6 | ) 7 | 8 | //IExec defines exec global interface, useful for testing 9 | type IExec interface { 10 | Run(dir string, command string, args ...string) ([]byte, error) 11 | RunInteractive(command string, args ...string) error 12 | DoGitClone(dir string, args ...string) (string, error) 13 | DoGitPush(dir string, args ...string) (string, error) 14 | DoGit(dir string, args ...string) (string, error) 15 | CreateFolderIfDoesNotExist(dir string) error 16 | OpenEditor(editor string, args ...string) error 17 | } 18 | 19 | //Exec struct 20 | type Exec struct{} 21 | 22 | //NewExec returns a new Exec 23 | func NewExec() *Exec { 24 | return &Exec{} 25 | } 26 | 27 | //CreateFolderIfDoesNotExist checks, will check that a folder exist and create the folder if it does not exist 28 | func (ex Exec) CreateFolderIfDoesNotExist(dir string) error { 29 | if _, err := os.Stat(dir); os.IsNotExist(err) { 30 | err := os.Mkdir(dir, os.ModePerm) 31 | if err != nil { 32 | return err 33 | } 34 | } 35 | 36 | return nil 37 | } 38 | 39 | //Run run command and return output 40 | func (ex Exec) Run(dir string, command string, args ...string) ([]byte, error) { 41 | cmd := exe.Command(command, args...) 42 | cmd.Dir = dir 43 | return cmd.CombinedOutput() 44 | } 45 | 46 | //RunInteractive run the command in interactive mode 47 | func (ex Exec) RunInteractive(command string, args ...string) error { 48 | cmd := exe.Command(command, args...) 49 | cmd.Stdout = os.Stdout 50 | cmd.Stderr = os.Stderr 51 | cmd.Stdin = os.Stdin 52 | return cmd.Run() 53 | } 54 | -------------------------------------------------------------------------------- /cmd/push_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | //TestPushCommand test the `ckp push` command 14 | func TestPushCommand(t *testing.T) { 15 | t.Run("push successfully", func(t *testing.T) { 16 | conf := createConfig(t) 17 | mockedExec := conf.Exec.(*mocks.MockIExec) 18 | writer := &bytes.Buffer{} 19 | conf.OutWriter = writer 20 | 21 | //Specify expectations 22 | gomock.InOrder( 23 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 24 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any(), gomock.Any()), 25 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 26 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 27 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any(), gomock.Any()), 28 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any(), gomock.Any()), 29 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 30 | ) 31 | 32 | command := cmd.NewPushCommand(conf) 33 | //Set writer 34 | command.SetOutput(conf.OutWriter) 35 | 36 | err := command.Execute() 37 | if err != nil { 38 | t.Errorf("Error: failed with %s", err) 39 | } 40 | 41 | exp := "\nckp store was pushed successfully\n" 42 | got := writer.String() 43 | if !assert.Equal(t, exp, got) { 44 | t.Errorf("expected failure with [%s], got [%s]", exp, got) 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/pull.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/briandowns/spinner" 8 | "github.com/elhmn/ckp/internal/config" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | //NewPullCommand create new cobra command for the push command 13 | func NewPullCommand(conf config.Config) *cobra.Command { 14 | command := &cobra.Command{ 15 | Use: "pull", 16 | Short: "pulls changes from remote storage repository", 17 | Long: `pulls changes from remote storage repository 18 | 19 | example: ckp pull 20 | `, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := pullCommand(conf); err != nil { 23 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 24 | return 25 | } 26 | }, 27 | } 28 | 29 | return command 30 | } 31 | 32 | func pullCommand(conf config.Config) error { 33 | //Setup spinner 34 | spin := spinner.New(spinner.CharSets[11], 100*time.Millisecond) 35 | spin.Start() 36 | defer spin.Stop() 37 | 38 | dir, err := config.GetStoreDirPath(conf) 39 | if err != nil { 40 | return fmt.Errorf("failed get repository path: %s", err) 41 | } 42 | 43 | storeFilePath, err := config.GetStoreFilePath(conf) 44 | if err != nil { 45 | return fmt.Errorf("failed get store file path: %s", err) 46 | } 47 | 48 | historyStoreFilePath, err := config.GetHistoryFilePath(conf) 49 | if err != nil { 50 | return fmt.Errorf("failed get history store file path: %s", err) 51 | } 52 | 53 | spin.Suffix = " pulling remote changes..." 54 | err = pullRemoteChanges(conf, dir, storeFilePath, historyStoreFilePath) 55 | if err != nil { 56 | return fmt.Errorf("failed to pull remote changes: %s", err) 57 | } 58 | spin.Suffix = " remote changes pulled" 59 | 60 | fmt.Fprintf(conf.OutWriter, "\nckp store was pulled successfully\n") 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/find_internal_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/internal/printers" 8 | "github.com/elhmn/ckp/internal/store" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDoesScriptContain(t *testing.T) { 13 | tests := []struct { 14 | Input string 15 | S store.Script 16 | Exp bool 17 | }{ 18 | {Input: "solution", Exp: true, S: store.Script{ 19 | Comment: "my comment", 20 | Code: store.Code{Content: "je suis con", Alias: "mon alias"}, 21 | Solution: store.Solution{Content: "ma solution"}, 22 | }, 23 | }, 24 | {Input: "comment", Exp: true, S: store.Script{ 25 | Comment: "my comment", 26 | Code: store.Code{Content: "je suis con", Alias: "mon alias"}, 27 | Solution: store.Solution{Content: "ma solution"}, 28 | }, 29 | }, 30 | {Input: "je suis", Exp: true, S: store.Script{ 31 | Comment: "my comment", 32 | Code: store.Code{Content: "je suis con", Alias: "mon alias"}, 33 | Solution: store.Solution{Content: "ma solution"}, 34 | }, 35 | }, 36 | {Input: "alias", Exp: true, S: store.Script{ 37 | Comment: "my comment", 38 | Code: store.Code{Content: "je suis con", Alias: "mon alias"}, 39 | Solution: store.Solution{Content: "ma solution"}, 40 | }, 41 | }, 42 | {Input: "comment alias", Exp: true, S: store.Script{ 43 | Comment: "my comment", 44 | Code: store.Code{Content: "je suis con", Alias: "mon alias"}, 45 | Solution: store.Solution{Content: "ma solution"}, 46 | }, 47 | }, 48 | } 49 | 50 | for i, test := range tests { 51 | t.Run(fmt.Sprintf("%d - test for \"%s\" equal %v", i, test.Input, test.Exp), func(t *testing.T) { 52 | assert.Equal(t, test.Exp, printers.DoesScriptContain(test.S, test.Input)) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cmd/rm_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestRmCommand(t *testing.T) { 14 | t.Run("make sure that it runs successfully", func(t *testing.T) { 15 | conf := createConfig(t) 16 | mockedExec := conf.Exec.(*mocks.MockIExec) 17 | 18 | writer := &bytes.Buffer{} 19 | conf.OutWriter = writer 20 | 21 | if err := setupFolder(conf); err != nil { 22 | t.Errorf("Error: failed with %s", err) 23 | } 24 | 25 | //Specify expectations 26 | gomock.InOrder( 27 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 28 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 29 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 30 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 31 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 32 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 33 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 34 | ) 35 | 36 | command := cmd.NewRmCommand(conf) 37 | //Set writer 38 | command.SetOutput(conf.OutWriter) 39 | 40 | //Set args 41 | command.SetArgs([]string{"hash-of-file-content"}) 42 | 43 | err := command.Execute() 44 | if err != nil { 45 | t.Errorf("Error: failed with %s", err) 46 | } 47 | 48 | got := writer.String() 49 | exp := "\nentry was removed successfully\n" 50 | assert.Equal(t, exp, got) 51 | 52 | //function call assert 53 | if err := deleteFolder(conf); err != nil { 54 | t.Errorf("Error: failed with %s", err) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /internal/history/history_test.go: -------------------------------------------------------------------------------- 1 | package history_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/elhmn/ckp/internal/files" 7 | "github.com/elhmn/ckp/internal/history" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetHistoryRecords(t *testing.T) { 12 | //set history files to fixtures 13 | origBashHistoryFile := history.BashHistoryFile 14 | origZshHistoryFile := history.ZshHistoryFile 15 | history.BashHistoryFile = "bash_history_test" 16 | history.ZshHistoryFile = "zsh_history_test" 17 | 18 | //create bash_history fixtures 19 | err := files.CopyFileToHomeDirectory(history.BashHistoryFile, "../../fixtures/history/bash_history_test") 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | //create zsh_history fixtures 25 | err = files.CopyFileToHomeDirectory(history.ZshHistoryFile, "../../fixtures/history/zsh_history_test") 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | 30 | t.Run("return history records", func(t *testing.T) { 31 | exp := []string{ 32 | "exit", 33 | "echo \"je suis con\"; echo \"tu es con\"", 34 | "pwd", 35 | "cd", 36 | "cat /dev/null > ~/.bash_history \\", 37 | "history", 38 | "echo \"je suis con\"; echo \"tu es con\"", 39 | "echo \"je suis con end of line\"", 40 | } 41 | records, err := history.GetHistoryRecords() 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | assert.Equal(t, exp, records) 46 | }) 47 | 48 | //Delete history tmp files 49 | if err := files.DeleteFileFromHomeDirectory(history.BashHistoryFile); err != nil { 50 | t.Error(err) 51 | } 52 | if err := files.DeleteFileFromHomeDirectory(history.ZshHistoryFile); err != nil { 53 | t.Error(err) 54 | } 55 | 56 | //Restore history path 57 | history.BashHistoryFile = origBashHistoryFile 58 | history.ZshHistoryFile = origZshHistoryFile 59 | } 60 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/blang/semver" 9 | "github.com/elhmn/ckp/internal/config" 10 | "github.com/rhysd/go-github-selfupdate/selfupdate" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | //NewUpdateCommand will update your binary to the latest release 15 | func NewUpdateCommand(conf config.Config) *cobra.Command { 16 | command := &cobra.Command{ 17 | Use: "update", 18 | Short: "Update the binary to the latest release", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | if err := updateCommand(conf); err != nil { 21 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 22 | return 23 | } 24 | }, 25 | } 26 | 27 | return command 28 | } 29 | 30 | func updateCommand(conf config.Config) error { 31 | return confirmAndUpdate(conf) 32 | } 33 | 34 | func confirmAndUpdate(conf config.Config) error { 35 | latest, found, err := selfupdate.DetectLatest(conf.Repository) 36 | if err != nil { 37 | return fmt.Errorf("Error occurred while detecting version: %s", err) 38 | } 39 | 40 | v := semver.MustParse(conf.Version) 41 | if !found || latest.Version.LTE(v) { 42 | fmt.Fprintf(conf.OutWriter, "Current version is the latest\n") 43 | return nil 44 | } 45 | 46 | fmt.Fprintf(conf.OutWriter, "Do you want to update to %s ? (y/n): \n", latest.Version) 47 | input, err := bufio.NewReader(os.Stdin).ReadString('\n') 48 | if err != nil || (input != "y\n" && input != "n\n") { 49 | fmt.Fprintf(conf.OutWriter, "Invalid input\n") 50 | return nil 51 | } 52 | if input == "n\n" { 53 | return nil 54 | } 55 | 56 | exe, err := os.Executable() 57 | if err != nil { 58 | return fmt.Errorf("Could not locate executable path: %s", err) 59 | } 60 | if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil { 61 | return fmt.Errorf("Error occurred while updating binary: %s", err) 62 | } 63 | 64 | fmt.Fprintf(conf.OutWriter, "Successfully updated to version %s\n", latest.Version) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /cmd/find.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/elhmn/ckp/internal/config" 7 | "github.com/elhmn/ckp/internal/store" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | //NewFindCommand display a prompt for you to enter the code or solution you are looking for 12 | func NewFindCommand(conf config.Config) *cobra.Command { 13 | command := &cobra.Command{ 14 | Use: "find", 15 | Aliases: []string{"f"}, 16 | Short: "find your code snippets and solutions", 17 | Long: `find your code snippets and solutions 18 | 19 | example: ckp find 20 | Will display a prompt for you to enter the code or solution you are looking for 21 | `, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | if err := findCommand(cmd, args, conf); err != nil { 24 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 25 | return 26 | } 27 | }, 28 | } 29 | 30 | command.PersistentFlags().Bool("from-history", false, `list code and solution records from history`) 31 | 32 | return command 33 | } 34 | 35 | func findCommand(cmd *cobra.Command, args []string, conf config.Config) error { 36 | if err := cmd.Flags().Parse(args); err != nil { 37 | return err 38 | } 39 | flags := cmd.Flags() 40 | fromHistory, err := flags.GetBool("from-history") 41 | if err != nil { 42 | return fmt.Errorf("could not parse `fromHistory` flag: %s", err) 43 | } 44 | 45 | //Get the store file path 46 | var storeFilePath string 47 | if !fromHistory { 48 | storeFilePath, err = config.GetStoreFilePath(conf) 49 | if err != nil { 50 | return fmt.Errorf("failed to get the store file path: %s", err) 51 | } 52 | } else { 53 | storeFilePath, err = config.GetHistoryFilePath(conf) 54 | if err != nil { 55 | return fmt.Errorf("failed to get the history store file path: %s", err) 56 | } 57 | } 58 | 59 | storeData, _, err := store.LoadStore(storeFilePath) 60 | if err != nil { 61 | return fmt.Errorf("failed to laod store: %s", err) 62 | } 63 | 64 | scripts := storeData.Scripts 65 | 66 | _, result, err := conf.Printers.SelectScriptEntry(scripts, store.EntryTypeAll) 67 | if err != nil { 68 | return fmt.Errorf("prompt failed %v", err) 69 | } 70 | 71 | fmt.Fprintf(conf.OutWriter, "\n%s", result) 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/history/history.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/mitchellh/go-homedir" 10 | ) 11 | 12 | var ( 13 | BashHistoryFile = ".bash_history" 14 | ZshHistoryFile = ".zsh_history" 15 | ) 16 | 17 | //GetHistoryRecords returns a list of code records 18 | //found in your history files 19 | func GetHistoryRecords() ([]string, error) { 20 | records := []string{} 21 | 22 | // Get bash history records 23 | bashRecords, err := getBashHistoryRecords() 24 | if err != nil { 25 | return nil, err 26 | } 27 | records = append(records, bashRecords...) 28 | 29 | //Get zsh history records 30 | zshRecords, err := getZshHistoryRecords() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | records = append(records, zshRecords...) 36 | return records, nil 37 | } 38 | 39 | func getBashHistoryRecords() ([]string, error) { 40 | data, err := readFileData(BashHistoryFile) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | f := func(c rune) bool { 46 | return c == '\n' 47 | } 48 | 49 | records := strings.FieldsFunc(data, f) 50 | return records, nil 51 | } 52 | 53 | func getZshHistoryRecords() ([]string, error) { 54 | data, err := readFileData(ZshHistoryFile) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | f := func(c rune) bool { 60 | return c == '\n' 61 | } 62 | lines := strings.FieldsFunc(data, f) 63 | records := []string{} 64 | for _, l := range lines { 65 | recordStartIndex := strings.Index(l, ";") 66 | if recordStartIndex < 0 { 67 | continue 68 | } 69 | 70 | line := l[recordStartIndex+1:] 71 | records = append(records, line) 72 | } 73 | return records, nil 74 | } 75 | 76 | func readFileData(filepath string) (string, error) { 77 | home, err := homedir.Dir() 78 | if err != nil { 79 | return "", fmt.Errorf("failed to read home directory: %s", err) 80 | } 81 | filepath = fmt.Sprintf("%s/%s", home, filepath) 82 | 83 | if _, err := os.Stat(filepath); os.IsNotExist(err) { 84 | return "", nil 85 | } 86 | 87 | data, err := ioutil.ReadFile(filepath) 88 | if err != nil { 89 | return "", fmt.Errorf("failed to read file %s: %s", filepath, err) 90 | } 91 | 92 | return string(data), nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/run_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestRunCommand(t *testing.T) { 14 | t.Run("make sure that it runs successfully", func(t *testing.T) { 15 | conf := createConfig(t) 16 | mockedExec := conf.Exec.(*mocks.MockIExec) 17 | writer := &bytes.Buffer{} 18 | conf.OutWriter = writer 19 | 20 | if err := setupFolder(conf); err != nil { 21 | t.Errorf("Error: failed with %s", err) 22 | } 23 | 24 | //Specify expectations 25 | gomock.InOrder( 26 | mockedExec.EXPECT().RunInteractive("bash", "-c", "echo \"mon code\"\n"), 27 | ) 28 | 29 | command := cmd.NewRunCommand(conf) 30 | //Set writer 31 | command.SetOutput(conf.OutWriter) 32 | 33 | //Set args 34 | command.SetArgs([]string{"hash-of-file-content"}) 35 | 36 | err := command.Execute() 37 | if err != nil { 38 | t.Errorf("Error: failed with %s", err) 39 | } 40 | 41 | //function call assert 42 | if err := deleteFolder(conf); err != nil { 43 | t.Errorf("Error: failed with %s", err) 44 | } 45 | }) 46 | 47 | t.Run("fail with solution id", func(t *testing.T) { 48 | conf := createConfig(t) 49 | mockedExec := conf.Exec.(*mocks.MockIExec) 50 | writer := &bytes.Buffer{} 51 | conf.OutWriter = writer 52 | 53 | if err := setupFolder(conf); err != nil { 54 | t.Errorf("Error: failed with %s", err) 55 | } 56 | 57 | //Specify expectations 58 | gomock.InOrder( 59 | mockedExec.EXPECT().RunInteractive("bash", "-c", "echo \"mon code\"\n").Times(0), 60 | ) 61 | 62 | command := cmd.NewRunCommand(conf) 63 | //Set writer 64 | command.SetOutput(conf.OutWriter) 65 | 66 | //Set args 67 | command.SetArgs([]string{"hash-of-file-content-2"}) 68 | 69 | err := command.Execute() 70 | if err != nil { 71 | t.Errorf("Error: failed with %s", err) 72 | } 73 | 74 | got := writer.String() 75 | exp := "might not be a code entry, nothing to run" 76 | assert.Contains(t, got, exp) 77 | 78 | //function call assert 79 | if err := deleteFolder(conf); err != nil { 80 | t.Errorf("Error: failed with %s", err) 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /mocks/IPrinters.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/printers/printers.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | store "github.com/elhmn/ckp/internal/store" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockIPrinters is a mock of IPrinters interface. 15 | type MockIPrinters struct { 16 | ctrl *gomock.Controller 17 | recorder *MockIPrintersMockRecorder 18 | } 19 | 20 | // MockIPrintersMockRecorder is the mock recorder for MockIPrinters. 21 | type MockIPrintersMockRecorder struct { 22 | mock *MockIPrinters 23 | } 24 | 25 | // NewMockIPrinters creates a new mock instance. 26 | func NewMockIPrinters(ctrl *gomock.Controller) *MockIPrinters { 27 | mock := &MockIPrinters{ctrl: ctrl} 28 | mock.recorder = &MockIPrintersMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockIPrinters) EXPECT() *MockIPrintersMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Confirm mocks base method. 38 | func (m *MockIPrinters) Confirm(message string) bool { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Confirm", message) 41 | ret0, _ := ret[0].(bool) 42 | return ret0 43 | } 44 | 45 | // Confirm indicates an expected call of Confirm. 46 | func (mr *MockIPrintersMockRecorder) Confirm(message interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Confirm", reflect.TypeOf((*MockIPrinters)(nil).Confirm), message) 49 | } 50 | 51 | // SelectScriptEntry mocks base method. 52 | func (m *MockIPrinters) SelectScriptEntry(scripts []store.Script, entryType string) (int, string, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "SelectScriptEntry", scripts, entryType) 55 | ret0, _ := ret[0].(int) 56 | ret1, _ := ret[1].(string) 57 | ret2, _ := ret[2].(error) 58 | return ret0, ret1, ret2 59 | } 60 | 61 | // SelectScriptEntry indicates an expected call of SelectScriptEntry. 62 | func (mr *MockIPrintersMockRecorder) SelectScriptEntry(scripts, entryType interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectScriptEntry", reflect.TypeOf((*MockIPrinters)(nil).SelectScriptEntry), scripts, entryType) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/elhmn/ckp/internal/config" 7 | "github.com/elhmn/ckp/internal/store" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | //NewRunCommand create new cobra command for the run command 12 | func NewRunCommand(conf config.Config) *cobra.Command { 13 | command := &cobra.Command{ 14 | Use: "run [code_id]", 15 | Short: "runs your code entries from the store", 16 | Long: `runs your code entries from the store 17 | 18 | example: ckp run 19 | Will prompt an interactive UI that will allow you to search and run 20 | a code entry 21 | 22 | example: ckp run 23 | Will run the entry corresponding the entry_id 24 | `, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | if err := runCommand(cmd, args, conf); err != nil { 27 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 28 | return 29 | } 30 | }, 31 | } 32 | 33 | command.PersistentFlags().Bool("from-history", false, `list code and solution records from history`) 34 | 35 | return command 36 | } 37 | 38 | func runCommand(cmd *cobra.Command, args []string, conf config.Config) error { 39 | var entryID string 40 | if len(args) >= 1 { 41 | entryID = args[0] 42 | } 43 | 44 | if err := cmd.Flags().Parse(args); err != nil { 45 | return err 46 | } 47 | flags := cmd.Flags() 48 | fromHistory, err := flags.GetBool("from-history") 49 | if err != nil { 50 | return fmt.Errorf("could not parse `fromHistory` flag: %s", err) 51 | } 52 | 53 | //Get the store file path 54 | var storeFilePath string 55 | if !fromHistory { 56 | storeFilePath, err = config.GetStoreFilePath(conf) 57 | if err != nil { 58 | return fmt.Errorf("failed to get the store file path: %s", err) 59 | } 60 | } else { 61 | storeFilePath, err = config.GetHistoryFilePath(conf) 62 | if err != nil { 63 | return fmt.Errorf("failed to get the history store file path: %s", err) 64 | } 65 | } 66 | 67 | _, storeData, _, err := loadStore(storeFilePath) 68 | if err != nil { 69 | return fmt.Errorf("failed to load the store: %s", err) 70 | } 71 | 72 | index, err := getScriptEntryIndex(conf, storeData.Scripts, entryID, store.EntryTypeCode) 73 | if err != nil { 74 | return fmt.Errorf("failed to get script `%s` entry index: %s", entryID, err) 75 | } 76 | 77 | if err := runCodeEntry(conf, storeData.Scripts, index); err != nil { 78 | return fmt.Errorf("failed to run code entry: %s", err) 79 | } 80 | return nil 81 | } 82 | 83 | func runCodeEntry(conf config.Config, scripts []store.Script, index int) error { 84 | script := scripts[index] 85 | if script.Code.Content == "" { 86 | return fmt.Errorf("might not be a code entry, nothing to run") 87 | } 88 | 89 | return conf.Exec.RunInteractive("bash", "-c", script.Code.Content) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/add_history_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/elhmn/ckp/cmd" 9 | "github.com/elhmn/ckp/internal/files" 10 | "github.com/elhmn/ckp/internal/history" 11 | "github.com/elhmn/ckp/mocks" 12 | "github.com/golang/mock/gomock" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestAddHistoryCommand(t *testing.T) { 17 | t.Run("make sure that is runs successfully", func(t *testing.T) { 18 | conf := createConfig(t) 19 | mockedExec := conf.Exec.(*mocks.MockIExec) 20 | writer := &bytes.Buffer{} 21 | conf.OutWriter = writer 22 | 23 | //Specify expectations 24 | gomock.InOrder( 25 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 26 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 27 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 28 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 29 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 30 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 31 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 32 | ) 33 | 34 | //set history files to fixtures 35 | origBashHistoryFile := history.BashHistoryFile 36 | origZshHistoryFile := history.ZshHistoryFile 37 | history.BashHistoryFile = "bash_history_test" 38 | history.ZshHistoryFile = "zsh_history_test" 39 | 40 | //create bash_history fixtures 41 | err := files.CopyFileToHomeDirectory(history.BashHistoryFile, "../fixtures/history/bash_history_test") 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | 46 | //create zsh_history fixtures 47 | err = files.CopyFileToHomeDirectory(history.ZshHistoryFile, "../fixtures/history/zsh_history_test") 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | 52 | if err := setupFolder(conf); err != nil { 53 | t.Errorf("Error: failed with %s", err) 54 | } 55 | 56 | commandName := "history" 57 | command := cmd.NewAddCommand(conf) 58 | //Set writer 59 | command.SetOutput(conf.OutWriter) 60 | 61 | //Set args 62 | command.SetArgs([]string{commandName}) 63 | 64 | if err := command.Execute(); err != nil { 65 | t.Errorf("Error: failed with %s", err) 66 | } 67 | 68 | got := writer.String() 69 | exp := "\nYour history was successfully added!\n" 70 | assert.Equal(t, exp, got) 71 | 72 | if err := deleteFolder(conf); err != nil { 73 | t.Errorf("Error: failed with %s", err) 74 | } 75 | 76 | //Delete history tmp files 77 | if err := files.DeleteFileFromHomeDirectory(history.BashHistoryFile); err != nil { 78 | fmt.Printf("Failed to remove file: %s\n", err) 79 | } 80 | 81 | if err := files.DeleteFileFromHomeDirectory(history.ZshHistoryFile); err != nil { 82 | fmt.Printf("Failed to remove file: %s\n", err) 83 | } 84 | 85 | //Restore history path 86 | history.BashHistoryFile = origBashHistoryFile 87 | history.ZshHistoryFile = origZshHistoryFile 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /cmd/list_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/elhmn/ckp/cmd" 9 | "github.com/elhmn/ckp/internal/config" 10 | ) 11 | 12 | func TestListCommand(t *testing.T) { 13 | t.Run("make sure that is runs successfully with limit 12", func(t *testing.T) { 14 | conf := createConfig(t) 15 | writer := &bytes.Buffer{} 16 | conf.OutWriter = writer 17 | 18 | if err := setupFolder(conf); err != nil { 19 | t.Errorf("Error: failed with %s", err) 20 | } 21 | 22 | command := cmd.NewListCommand(conf) 23 | //Set writer 24 | command.SetOutput(conf.OutWriter) 25 | 26 | //Set args 27 | command.SetArgs([]string{"-l", "12"}) 28 | 29 | err := command.Execute() 30 | if err != nil { 31 | t.Errorf("Error: failed with %s", err) 32 | } 33 | 34 | if err := deleteFolder(conf); err != nil { 35 | t.Errorf("Error: failed with %s", err) 36 | } 37 | }) 38 | 39 | t.Run("make sure that is runs successfully with --all flag set", func(t *testing.T) { 40 | conf := createConfig(t) 41 | writer := &bytes.Buffer{} 42 | conf.OutWriter = writer 43 | 44 | if err := setupFolder(conf); err != nil { 45 | t.Errorf("Error: failed with %s", err) 46 | } 47 | 48 | command := cmd.NewListCommand(conf) 49 | //Set writer 50 | command.SetOutput(conf.OutWriter) 51 | 52 | //Set args 53 | command.SetArgs([]string{"--all"}) 54 | 55 | err := command.Execute() 56 | if err != nil { 57 | t.Errorf("Error: failed with %s", err) 58 | } 59 | 60 | if err := deleteFolder(conf); err != nil { 61 | t.Errorf("Error: failed with %s", err) 62 | } 63 | }) 64 | 65 | t.Run("make sure that is runs successfully on history, with --all flag set", func(t *testing.T) { 66 | conf := createConfig(t) 67 | writer := &bytes.Buffer{} 68 | conf.OutWriter = writer 69 | 70 | if err := setupFolder(conf); err != nil { 71 | t.Errorf("Error: failed with %s", err) 72 | } 73 | 74 | command := cmd.NewListCommand(conf) 75 | //Set writer 76 | command.SetOutput(conf.OutWriter) 77 | 78 | //Set args 79 | command.SetArgs([]string{"--all", "--from-history"}) 80 | 81 | err := command.Execute() 82 | if err != nil { 83 | t.Errorf("Error: failed with %s", err) 84 | } 85 | 86 | if err := deleteFolder(conf); err != nil { 87 | t.Errorf("Error: failed with %s", err) 88 | } 89 | }) 90 | } 91 | 92 | func BenchmarkListFromHistory(b *testing.B) { 93 | conf := config.NewDefaultConfig(config.Options{Version: "0.0.0+dev"}) 94 | writer := &bytes.Buffer{} 95 | conf.OutWriter = writer 96 | conf.CKPDir = ".ckp_test" 97 | 98 | if err := setupFolder(conf); err != nil { 99 | fmt.Printf("Error: failed with %s", err) 100 | } 101 | 102 | command := cmd.NewListCommand(conf) 103 | //Set writer 104 | command.SetOutput(conf.OutWriter) 105 | 106 | //Set args 107 | command.SetArgs([]string{"--all", "--from-history"}) 108 | 109 | err := command.Execute() 110 | if err != nil { 111 | fmt.Printf("Error: failed with %s", err) 112 | } 113 | 114 | if err := deleteFolder(conf); err != nil { 115 | fmt.Printf("Error: failed with %s", err) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /cmd/add_solution_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestAddSolutionCommand(t *testing.T) { 14 | t.Run("make sure that is runs successfully", func(t *testing.T) { 15 | conf := createConfig(t) 16 | mockedExec := conf.Exec.(*mocks.MockIExec) 17 | writer := &bytes.Buffer{} 18 | conf.OutWriter = writer 19 | 20 | if err := setupFolder(conf); err != nil { 21 | t.Errorf("Error: failed with %s", err) 22 | } 23 | 24 | //Specify expectations 25 | gomock.InOrder( 26 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 27 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 28 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 29 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 30 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 31 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 32 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 33 | ) 34 | 35 | commandName := "solution" 36 | command := cmd.NewAddCommand(conf) 37 | //Set writer 38 | command.SetOutput(conf.OutWriter) 39 | 40 | //Set args 41 | command.SetArgs([]string{commandName, 42 | "our solution", 43 | "--comment", "a_comment", 44 | }) 45 | 46 | err := command.Execute() 47 | if err != nil { 48 | t.Errorf("Error: failed with %s", err) 49 | } 50 | 51 | got := writer.String() 52 | exp := "\nYour solution was successfully added!\n" 53 | assert.Contains(t, got, exp) 54 | 55 | if err := deleteFolder(conf); err != nil { 56 | t.Errorf("Error: failed with %s", err) 57 | } 58 | }) 59 | 60 | t.Run("make sure that is runs successfully without solution argument", func(t *testing.T) { 61 | conf := createConfig(t) 62 | mockedExec := conf.Exec.(*mocks.MockIExec) 63 | writer := &bytes.Buffer{} 64 | conf.OutWriter = writer 65 | 66 | if err := setupFolder(conf); err != nil { 67 | t.Errorf("Error: failed with %s", err) 68 | } 69 | 70 | //Specify expectations 71 | gomock.InOrder( 72 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 73 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 74 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 75 | mockedExec.EXPECT().OpenEditor(gomock.Any(), gomock.Any()).Return(nil), 76 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 77 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 78 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 79 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 80 | ) 81 | 82 | commandName := "solution" 83 | command := cmd.NewAddCommand(conf) 84 | //Set writer 85 | command.SetOutput(conf.OutWriter) 86 | 87 | //Set args 88 | command.SetArgs([]string{commandName, 89 | "--comment", "a_comment", 90 | }) 91 | 92 | err := command.Execute() 93 | if err != nil { 94 | t.Errorf("Error: failed with %s", err) 95 | } 96 | 97 | got := writer.String() 98 | exp := "\nYour solution was successfully added!\n" 99 | assert.Contains(t, got, exp) 100 | 101 | if err := deleteFolder(conf); err != nil { 102 | t.Errorf("Error: failed with %s", err) 103 | } 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /fixtures/list.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/elhmn/ckp/internal/store" 7 | ) 8 | 9 | //GetListWithMoreThan10Elements returns list of scripts that contains more than 10 elements 10 | func GetListWithMoreThan10Elements() []store.Script { 11 | creationTimeFix, err := time.Parse(time.RFC1123, "Thu, 13 May 2021 11:36:17 CEST") 12 | if err != nil { 13 | return nil 14 | } 15 | 16 | updateTimeFix, err := time.Parse(time.RFC1123, "Thu, 13 May 2021 11:38:17 CEST") 17 | if err != nil { 18 | return nil 19 | } 20 | 21 | return []store.Script{ 22 | {ID: "ID_1", CreationTime: creationTimeFix, UpdateTime: updateTimeFix, Comment: "comment_1", Code: store.Code{Content: "code_content_1"}}, 23 | {ID: "ID_2", CreationTime: creationTimeFix, UpdateTime: updateTimeFix, Comment: "Comment_2", Code: store.Code{Content: "code_content_2", Alias: "Alias_2"}}, 24 | {ID: "ID_3", CreationTime: creationTimeFix, UpdateTime: updateTimeFix, Code: store.Code{Content: "code_content_3"}}, 25 | {ID: "ID_4", CreationTime: creationTimeFix, UpdateTime: updateTimeFix, Comment: "Comment_4", Solution: store.Solution{Content: "solution_content_4"}}, 26 | {ID: "ID_5", CreationTime: creationTimeFix, UpdateTime: updateTimeFix, Solution: store.Solution{Content: "solution_content_5"}}, 27 | } 28 | } 29 | 30 | func GetPrintListWithMoreThan10Elements() string { 31 | return `ID: ID_1 32 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 33 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 34 | Type: Code 35 | Comment: comment_1 36 | Code: code_content_1 37 | 38 | ID: ID_2 39 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 40 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 41 | Type: Code 42 | Alias: Alias_2 43 | Comment: Comment_2 44 | Code: code_content_2 45 | 46 | ID: ID_3 47 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 48 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 49 | Type: Code 50 | Code: code_content_3 51 | 52 | ID: ID_4 53 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 54 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 55 | Type: Solution 56 | Comment: Comment_4 57 | Solution: solution_content_4 58 | 59 | ID: ID_5 60 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 61 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 62 | Type: Solution 63 | Solution: solution_content_5 64 | 65 | ` 66 | } 67 | 68 | func GetPrintListWithLessThan2Elements() string { 69 | return `ID: ID_1 70 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 71 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 72 | Type: Code 73 | Comment: comment_1 74 | Code: code_content_1 75 | 76 | ID: ID_2 77 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 78 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 79 | Type: Code 80 | Alias: Alias_2 81 | Comment: Comment_2 82 | Code: code_content_2 83 | 84 | ` 85 | } 86 | 87 | func GetPrintListOnlyCode() string { 88 | return `ID: ID_1 89 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 90 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 91 | Type: Code 92 | Comment: comment_1 93 | Code: code_content_1 94 | 95 | ID: ID_2 96 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 97 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 98 | Type: Code 99 | Alias: Alias_2 100 | Comment: Comment_2 101 | Code: code_content_2 102 | 103 | ID: ID_3 104 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 105 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 106 | Type: Code 107 | Code: code_content_3 108 | 109 | ` 110 | } 111 | 112 | func GetPrintListOnlySolution() string { 113 | return `ID: ID_4 114 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 115 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 116 | Type: Solution 117 | Comment: Comment_4 118 | Solution: solution_content_4 119 | 120 | ID: ID_5 121 | CreationTime: Thu, 13 May 2021 11:36:17 CEST 122 | UpdateTime: Thu, 13 May 2021 11:38:17 CEST 123 | Type: Solution 124 | Solution: solution_content_5 125 | 126 | ` 127 | } 128 | -------------------------------------------------------------------------------- /cmd/init_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/elhmn/ckp/cmd" 10 | "github.com/elhmn/ckp/mocks" 11 | "github.com/golang/mock/gomock" 12 | ) 13 | 14 | //TestInitCommand test the `ckp init` command 15 | func TestInitCommand(t *testing.T) { 16 | fakeRemoteFolder := "https://github.com/elhmn/fakefolder" 17 | 18 | t.Run("initialised successfully", func(t *testing.T) { 19 | conf := createConfig(t) 20 | mockedExec := conf.Exec.(*mocks.MockIExec) 21 | writer := &bytes.Buffer{} 22 | conf.OutWriter = writer 23 | 24 | //Specify expectations 25 | gomock.InOrder( 26 | mockedExec.EXPECT().CreateFolderIfDoesNotExist(gomock.Any()), 27 | mockedExec.EXPECT().DoGitClone(gomock.Any(), gomock.Any(), gomock.Any()), 28 | mockedExec.EXPECT().DoGit(gomock.Any(), "log"), 29 | mockedExec.EXPECT().DoGit(gomock.Any(), "branch", "-M", conf.MainBranch), 30 | mockedExec.EXPECT().DoGitPush(gomock.Any(), "origin", conf.MainBranch, "-f"), 31 | ) 32 | 33 | if err := setupFolder(conf); err != nil { 34 | t.Errorf("Error: failed with %s", err) 35 | } 36 | 37 | command := cmd.NewInitCommand(conf) 38 | //Set writer 39 | command.SetOutput(conf.OutWriter) 40 | 41 | //Set args 42 | command.SetArgs([]string{fakeRemoteFolder}) 43 | 44 | err := command.Execute() 45 | if err != nil { 46 | t.Errorf("Error: failed with %s", err) 47 | } 48 | 49 | if err := deleteFolder(conf); err != nil { 50 | t.Errorf("Error: failed with %s", err) 51 | } 52 | }) 53 | 54 | t.Run("failed to create folder", func(t *testing.T) { 55 | conf := createConfig(t) 56 | mockedExec := conf.Exec.(*mocks.MockIExec) 57 | writer := &bytes.Buffer{} 58 | conf.OutWriter = writer 59 | 60 | //Specify expectations 61 | gomock.InOrder( 62 | mockedExec.EXPECT().CreateFolderIfDoesNotExist(gomock.Any()).Return(fmt.Errorf("failed to create folder")), 63 | ) 64 | 65 | if err := setupFolder(conf); err != nil { 66 | t.Errorf("Error: failed with %s", err) 67 | } 68 | 69 | exp := "failed to create folder" 70 | 71 | command := cmd.NewInitCommand(conf) 72 | //Set writer 73 | command.SetOutput(conf.OutWriter) 74 | 75 | //Set args 76 | command.SetArgs([]string{fakeRemoteFolder}) 77 | 78 | err := command.Execute() 79 | if err != nil { 80 | t.Errorf("Error: failed with %s", err) 81 | } 82 | 83 | got := writer.String() 84 | if !strings.Contains(got, exp) { 85 | t.Errorf("expected failure with [%s], got [%s]", exp, got) 86 | } 87 | 88 | if err := deleteFolder(conf); err != nil { 89 | t.Errorf("Error: failed with %s", err) 90 | } 91 | }) 92 | 93 | t.Run("failed to clone remote repository", func(t *testing.T) { 94 | conf := createConfig(t) 95 | mockedExec := conf.Exec.(*mocks.MockIExec) 96 | writer := &bytes.Buffer{} 97 | conf.OutWriter = writer 98 | 99 | //Specify expectations 100 | gomock.InOrder( 101 | mockedExec.EXPECT().CreateFolderIfDoesNotExist(gomock.Any()), 102 | mockedExec.EXPECT().DoGitClone(gomock.Any(), gomock.Any(), gomock.Any()).Return("", fmt.Errorf("failed to clone remote repository")), 103 | ) 104 | 105 | if err := setupFolder(conf); err != nil { 106 | t.Errorf("Error: failed with %s", err) 107 | } 108 | 109 | exp := "failed to clone remote repository" 110 | 111 | command := cmd.NewInitCommand(conf) 112 | //Set writer 113 | command.SetOutput(conf.OutWriter) 114 | 115 | //Set args 116 | command.SetArgs([]string{fakeRemoteFolder}) 117 | 118 | err := command.Execute() 119 | if err != nil { 120 | t.Errorf("Error: failed with %s", err) 121 | } 122 | 123 | got := writer.String() 124 | if !strings.Contains(got, exp) { 125 | t.Errorf("expected failure with [%s], got [%s]", exp, got) 126 | } 127 | 128 | if err := deleteFolder(conf); err != nil { 129 | t.Errorf("Error: failed with %s", err) 130 | } 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | 8 | "github.com/elhmn/ckp/internal/config" 9 | "github.com/mitchellh/go-homedir" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | //NewInitCommand create new cobra command for the init command 14 | func NewInitCommand(conf config.Config) *cobra.Command { 15 | command := &cobra.Command{ 16 | Use: "init ", 17 | Short: "initialise ckp storage repository", 18 | Long: `will initialise a storage repository 19 | 20 | example: ckp init 21 | This git repository will be used as your storage 22 | `, 23 | Args: cobra.ExactArgs(1), 24 | Run: func(cmd *cobra.Command, args []string) { 25 | storageFolder := args[0] 26 | 27 | if err := initCommand(conf, storageFolder); err != nil { 28 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 29 | return 30 | } 31 | }, 32 | } 33 | 34 | return command 35 | } 36 | 37 | func initCommand(conf config.Config, remoteStorageFolder string) error { 38 | //Setup spinner 39 | conf.Spin.Start() 40 | defer conf.Spin.Stop() 41 | 42 | home, err := homedir.Dir() 43 | if err != nil { 44 | return fmt.Errorf("failed to read home directory: %s", err) 45 | } 46 | 47 | //Create ckp folder if it does not exist 48 | dir := fmt.Sprintf("%s/%s", home, conf.CKPDir) 49 | err = conf.Exec.CreateFolderIfDoesNotExist(dir) 50 | if err != nil { 51 | return fmt.Errorf("failed to create `%s` directory: %s", dir, err) 52 | } 53 | fmt.Fprintf(conf.OutWriter, "`%s` directory was created\n", dir) 54 | 55 | //clone remote storage folder 56 | fmt.Fprintf(conf.OutWriter, "Initialising `%s` remote storage folder\n", remoteStorageFolder) 57 | out, err := conf.Exec.DoGitClone(dir, remoteStorageFolder, conf.CKPStorageFolder) 58 | if err != nil { 59 | return fmt.Errorf("failed to clone `%s`: %s\n%s", remoteStorageFolder, err, out) 60 | } 61 | 62 | //Get local storage folder 63 | localStorageFolder := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, conf.CKPStorageFolder) 64 | 65 | //add an empty commit if the repository has no commits 66 | { 67 | out, _ := conf.Exec.DoGit(localStorageFolder, "log") 68 | if strings.Contains(out, "does not have any commits yet") { 69 | storeFilePath, err := config.GetStoreFilePath(conf) 70 | if err != nil { 71 | return fmt.Errorf("failed get store file path: %s", err) 72 | } 73 | 74 | //Create the storage file 75 | if err = ioutil.WriteFile(storeFilePath, []byte{}, 0666); err != nil { 76 | return fmt.Errorf("failed to write to file %s: %s", storeFilePath, err) 77 | } 78 | 79 | historyStoreFilePath, err := config.GetHistoryFilePath(conf) 80 | if err != nil { 81 | return fmt.Errorf("failed get history store file path: %s", err) 82 | } 83 | 84 | //Create the history storage file 85 | if err = ioutil.WriteFile(historyStoreFilePath, []byte{}, 0666); err != nil { 86 | return fmt.Errorf("failed to write to file %s: %s", historyStoreFilePath, err) 87 | } 88 | 89 | //Add storage file 90 | out, err = conf.Exec.DoGit(localStorageFolder, "add", storeFilePath, historyStoreFilePath) 91 | if err != nil { 92 | return fmt.Errorf("failed to add changes: %s: %s", err, out) 93 | } 94 | 95 | //Create first commit 96 | out, err := conf.Exec.DoGit(localStorageFolder, "commit", "-m", "first commit") 97 | if err != nil { 98 | return fmt.Errorf("failed to rename branch to `%s`: %s:%s", conf.MainBranch, err, out) 99 | } 100 | } 101 | } 102 | 103 | //checkout rename branch 104 | out, err = conf.Exec.DoGit(localStorageFolder, "branch", "-M", conf.MainBranch) 105 | if err != nil { 106 | return fmt.Errorf("failed to rename branch to `%s`: %s:%s", conf.MainBranch, err, out) 107 | } 108 | 109 | //push renamed branch to remote 110 | out, err = conf.Exec.DoGitPush(localStorageFolder, "origin", conf.MainBranch, "-f") 111 | if err != nil { 112 | return fmt.Errorf("failed to push `%s` branch: %s:%s", conf.MainBranch, err, out) 113 | } 114 | 115 | fmt.Fprintf(conf.OutWriter, "`%s` remote storage folder, Initialised\n", remoteStorageFolder) 116 | 117 | fmt.Fprintf(conf.OutWriter, "ckp successfully initialised\n") 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /cmd/rm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/elhmn/ckp/internal/config" 8 | "github.com/elhmn/ckp/internal/store" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | //NewRmCommand create new cobra command for the rm command 13 | func NewRmCommand(conf config.Config) *cobra.Command { 14 | command := &cobra.Command{ 15 | Use: "rm [code_id | solution_id]", 16 | Short: "removes code or solution entries from the store", 17 | Long: `removes code or solution entries from the store 18 | 19 | example: ckp rm 20 | Will prompt an interactive UI that will allow you to search and delete 21 | a code or solution entry 22 | 23 | example: ckp rm 24 | Will remove the entry corresponding the entry_id 25 | `, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | if err := rmCommand(cmd, args, conf); err != nil { 28 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 29 | return 30 | } 31 | }, 32 | } 33 | 34 | command.PersistentFlags().Bool("from-history", false, `list code and solution records from history`) 35 | 36 | return command 37 | } 38 | 39 | func rmCommand(cmd *cobra.Command, args []string, conf config.Config) error { 40 | var entryID string 41 | if len(args) >= 1 { 42 | entryID = args[0] 43 | } 44 | 45 | if err := cmd.Flags().Parse(args); err != nil { 46 | return err 47 | } 48 | flags := cmd.Flags() 49 | fromHistory, err := flags.GetBool("from-history") 50 | if err != nil { 51 | return fmt.Errorf("could not parse `fromHistory` flag: %s", err) 52 | } 53 | 54 | //Setup spinner 55 | conf.Spin.Start() 56 | defer conf.Spin.Stop() 57 | 58 | dir, err := config.GetStoreDirPath(conf) 59 | if err != nil { 60 | return fmt.Errorf("failed get repository path: %s", err) 61 | } 62 | 63 | //Get the store file path 64 | var storeFilePath string 65 | if !fromHistory { 66 | storeFilePath, err = config.GetStoreFilePath(conf) 67 | if err != nil { 68 | return fmt.Errorf("failed to get the store file path: %s", err) 69 | } 70 | } else { 71 | storeFilePath, err = config.GetHistoryFilePath(conf) 72 | if err != nil { 73 | return fmt.Errorf("failed to get the history store file path: %s", err) 74 | } 75 | } 76 | 77 | conf.Spin.Message(" pulling remote changes...") 78 | err = pullRemoteChanges(conf, dir, storeFilePath) 79 | if err != nil { 80 | return fmt.Errorf("failed to pull remote changes: %s", err) 81 | } 82 | conf.Spin.Message(" remote changes pulled") 83 | 84 | conf.Spin.Message(" removing changes") 85 | storeFile, storeData, storeBytes, err := loadStore(storeFilePath) 86 | if err != nil { 87 | return fmt.Errorf("failed to load the store: %s", err) 88 | } 89 | 90 | index, err := getScriptEntryIndex(conf, storeData.Scripts, entryID, store.EntryTypeAll) 91 | if err != nil { 92 | return fmt.Errorf("failed to get script `%s` entry index: %s", entryID, err) 93 | } 94 | 95 | //Remove script entry 96 | storeData.Scripts = removeScriptEntry(storeData.Scripts, index) 97 | 98 | tempFile, err := createTempFile(conf, storeBytes) 99 | if err != nil { 100 | return fmt.Errorf("failed to create tempFile: %s", err) 101 | } 102 | 103 | //Save storeData in store 104 | if err := saveStore(storeData, storeBytes, storeFile, tempFile); err != nil { 105 | return fmt.Errorf("failed to save store in %s: %s", storeFile, err) 106 | } 107 | 108 | //Delete the temporary file 109 | if err := os.RemoveAll(tempFile); err != nil { 110 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 111 | } 112 | 113 | conf.Spin.Message(" pushing local changes...") 114 | err = pushLocalChanges(conf, dir, commitRemoveAction, storeFilePath) 115 | if err != nil { 116 | return fmt.Errorf("failed to push local changes: %s", err) 117 | } 118 | conf.Spin.Message(" local changes pushed") 119 | 120 | fmt.Fprintf(conf.OutWriter, "\nentry was removed successfully\n") 121 | return nil 122 | } 123 | 124 | func removeScriptEntry(scripts []store.Script, index int) []store.Script { 125 | return append(scripts[:index], scripts[index+1:]...) 126 | } 127 | 128 | func getScriptEntryIndex(conf config.Config, scripts []store.Script, entryID string, entryType string) (int, error) { 129 | if entryID == "" { 130 | conf.Spin.Stop() 131 | index, _, err := conf.Printers.SelectScriptEntry(scripts, entryType) 132 | if err != nil { 133 | return index, fmt.Errorf("failed to select entry: %s", err) 134 | } 135 | return index, nil 136 | } 137 | 138 | for index, s := range scripts { 139 | if entryID == s.ID { 140 | return index, nil 141 | } 142 | } 143 | 144 | return -1, fmt.Errorf("entry not found") 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codekeeper : (ckp) 2 | CLI that helps you store and reuse your history and one liner scripts from anywhere, better than gists. 3 | 4 | ## Overview 5 | 6 | If you ever found yourself using a bunch of complex scripts or useful bash oneliners and you find it hard to manually add them to a file, send them to a server and then fetch this scripts to that new machine you have recently acquired or ssh-ed into, this tool is for you. Store and fetch your scripts, your terminal history and your notes from anywhere. 7 | 8 | ![ckp_demo](https://user-images.githubusercontent.com/5704817/120272338-39377a80-c2ad-11eb-9058-a16f98745bb1.gif) 9 | 10 | ## Prerequisite 11 | `ckp` uses several dependencies such as: 12 | 1. `git` version >= 2.24.3 you can follow this [steps](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) to install git 13 | 2. only `bash` compatible commands can be run using `ckp`, you can use the tool without `bash` but you won't be able to run your commands using the CLI 14 | 15 | ## Install 16 | 17 | #### Using the install script: 18 | 19 | Run 20 | ```sh 21 | $> curl https://raw.githubusercontent.com/elhmn/ckp/master/install.sh | bash 22 | ``` 23 | It will create a `./bin/ckp` binary on your machine 24 | In order to run the command add it to your `/usr/local/bin` 25 | ```sh 26 | $> cp ./bin/ckp /usr/local/bin 27 | ``` 28 | 29 | #### Using homebrew: 30 | 31 | Run 32 | ```sh 33 | $> brew tap elhmn/ckp https://github.com/elhmn/ckp 34 | $> brew install ckp 35 | ``` 36 | 37 | #### Download 38 | 39 | Download the lastest version [here](https://github.com/elhmn/ckp/releases) 40 | Then copy the binary to your system binary `/usr/local/bin` folder 41 | 42 | 43 | ## Usage 44 | 45 | #### How to `Init`-ialize `ckp` 46 | 47 | 1. You first need to create an empty git repository that `ckp` will use as a storage. we higly recommend to keep this repository private 48 | 49 | 2. Once the repository is created you can initialise `ckp` using the init command. 50 | Copy the ssh or https url and pass it as an argument to the `ckp init` command 51 | 52 | ```sh 53 | $> ckp init git@github.com:elhmn/store.git 54 | ``` 55 | 56 | This will create a `~/.ckp` folder, and clone the storage repository 57 | 58 | #### How to set your text editor 59 | 60 | Vim is the default text editor to use a different code editor you might need to create a `~/.ckp/config.yaml` file, 61 | then open the file and set the `editor` field as follows. 62 | 63 | ```yaml 64 | editor: nano 65 | ``` 66 | 67 | 68 | #### How to `Add` your scripts and solutions 69 | 70 | The `add code` command will store your script as a code entry in ckp. 71 | 72 | ```sh 73 | $> ckp add code 'echo say hi!' --alias="sayHi" --comment="a script that says hi" 74 | ``` 75 | 76 | The `add solution` command will store your script as a solution entry in ckp. 77 | 78 | ```sh 79 | $> ckp add solution 'https://career-ladders.dev/engineering/' --comment="carreer ladders" 80 | ``` 81 | 82 | #### How to add scripts from my `bash_history` or `zh_history` 83 | 84 | The `add history` command will read scripts from your history files and store them in ckp. 85 | the `--skip-secrets` flag will force ckp to skip scripts that potentially contains secrets. 86 | 87 | ```sh 88 | $> ckp add history --skip-secrets 89 | ``` 90 | 91 | #### How to `Push` your scripts to your remote storage repository 92 | 93 | The `push` command will be commited and pushed to your remote repoitory. 94 | 95 | ```sh 96 | $> ckp push 97 | ``` 98 | 99 | #### How to `Pull` your scripts from your remote storage repository 100 | 101 | The `pull` command will pull changes from your remote storage repository. 102 | 103 | ```sh 104 | $> ckp pull 105 | ``` 106 | 107 | #### How to `Find` a script or solution 108 | 109 | The `find` command will prompt a search and selection UI, that can be used to find. 110 | 111 | ```sh 112 | $> ckp find 113 | ``` 114 | 115 | To find a script in your history. 116 | 117 | ```sh 118 | $> ckp find --from-history 119 | ``` 120 | 121 | 122 | #### How to `Run` a script or solution 123 | 124 | The `run` command will prompt a search and selection UI, that can be used to find and run a specific script. 125 | 126 | ```sh 127 | $> ckp run 128 | ``` 129 | 130 | To run a script from your history. 131 | 132 | ```sh 133 | $> ckp run --from-history 134 | ``` 135 | 136 | 137 | #### How to `Remove` a script or solution 138 | 139 | The `rm` command will prompt a search and selection UI, that can be used to find and run a specific script. 140 | 141 | ```sh 142 | $> ckp rm 143 | ``` 144 | 145 | To remove a script from your history. 146 | 147 | ```sh 148 | $> ckp rm --from-history 149 | ``` 150 | 151 | 152 | ## License 153 | 154 | MIT. 155 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/elhmn/ckp/internal/exec" 9 | "github.com/elhmn/ckp/internal/printers" 10 | "github.com/mitchellh/go-homedir" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | const ( 15 | StoreFileName = "repo/store.yaml" 16 | HistoryFileName = "repo/history_store.yaml" 17 | StoreTempFileName = "repo/.temp_store.yaml" 18 | StoreHistoryTempFileName = "repo/.temp_history_store.yaml" 19 | 20 | MainBranch = "main" 21 | ) 22 | 23 | //Config contains the entire cli dependencies 24 | type Config struct { 25 | Version string 26 | Viper viper.Viper 27 | Exec exec.IExec 28 | CKPDir string 29 | CKPStorageFolder string 30 | Spin printers.ISpinner 31 | Printers printers.IPrinters 32 | Repository string 33 | 34 | //MainBranch is a your remote repository main branch 35 | MainBranch string 36 | 37 | //WorkingBranch is `ckp` local working branch 38 | //instead of using your main branch `ckp` uses a separate 39 | //branch locally to facilite diff checks between your local 40 | //and remote changes 41 | WorkingBranch string 42 | 43 | //io Writers useful for testing 44 | OutWriter io.Writer 45 | ErrWriter io.Writer 46 | } 47 | 48 | //Options config options 49 | type Options struct { 50 | Version string 51 | } 52 | 53 | //NewDefaultConfig creates a new default config 54 | func NewDefaultConfig(opt Options) Config { 55 | conf := Config{ 56 | Exec: exec.NewExec(), 57 | Spin: printers.NewSpinner(), 58 | Printers: printers.NewPrinters(), 59 | OutWriter: os.Stdout, 60 | ErrWriter: os.Stderr, 61 | CKPDir: ".ckp", 62 | CKPStorageFolder: "repo", 63 | MainBranch: MainBranch, 64 | WorkingBranch: "working-" + MainBranch, 65 | Version: opt.Version, 66 | Repository: "elhmn/ckp", 67 | } 68 | 69 | conf.Viper = setupViper(conf) 70 | return conf 71 | } 72 | 73 | func setupViper(conf Config) viper.Viper { 74 | v := viper.New() 75 | v.SetConfigName("config") 76 | v.SetConfigType("yaml") 77 | 78 | dir, err := GetDirPath(conf) 79 | if err != nil { 80 | return viper.Viper{} 81 | } 82 | 83 | v.AddConfigPath(dir) 84 | err = v.ReadInConfig() 85 | if err != nil { 86 | return viper.Viper{} 87 | } 88 | 89 | return *v 90 | } 91 | 92 | //GetStoreFilePath get the store file path from config 93 | func GetStoreFilePath(conf Config) (string, error) { 94 | home, err := homedir.Dir() 95 | if err != nil { 96 | return "", fmt.Errorf("failed to read home directory: %s", err) 97 | } 98 | 99 | storepath := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, StoreFileName) 100 | return storepath, nil 101 | } 102 | 103 | //GetHistoryFilePath get the store file path from config 104 | func GetHistoryFilePath(conf Config) (string, error) { 105 | home, err := homedir.Dir() 106 | if err != nil { 107 | return "", fmt.Errorf("failed to read home directory: %s", err) 108 | } 109 | 110 | storepath := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, HistoryFileName) 111 | return storepath, nil 112 | } 113 | 114 | //GetTempStoreFilePath get the temporary store file path from config 115 | func GetTempStoreFilePath(conf Config) (string, error) { 116 | home, err := homedir.Dir() 117 | if err != nil { 118 | return "", fmt.Errorf("failed to read home directory: %s", err) 119 | } 120 | 121 | storepath := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, StoreTempFileName) 122 | return storepath, nil 123 | } 124 | 125 | //GetTempHistoryStoreFilePath get the temporary store file path from config 126 | func GetTempHistoryStoreFilePath(conf Config) (string, error) { 127 | home, err := homedir.Dir() 128 | if err != nil { 129 | return "", fmt.Errorf("failed to read home directory: %s", err) 130 | } 131 | 132 | storepath := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, StoreHistoryTempFileName) 133 | return storepath, nil 134 | } 135 | 136 | //GetStoreDirPath returns the path of the ckp store git repository 137 | func GetStoreDirPath(conf Config) (string, error) { 138 | home, err := homedir.Dir() 139 | if err != nil { 140 | return "", fmt.Errorf("failed to read home directory: %s", err) 141 | } 142 | 143 | dir := fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, conf.CKPStorageFolder) 144 | return dir, nil 145 | } 146 | 147 | //GetDirPath returns the path of the .ckp folder 148 | func GetDirPath(conf Config) (string, error) { 149 | home, err := homedir.Dir() 150 | if err != nil { 151 | return "", fmt.Errorf("failed to read home directory: %s", err) 152 | } 153 | 154 | dir := fmt.Sprintf("%s/%s", home, conf.CKPDir) 155 | return dir, nil 156 | } 157 | -------------------------------------------------------------------------------- /internal/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | const ( 16 | autoGeneratedHeader = `## File generated by ckp. DO NOT EDIT 17 | ## 18 | ## author: elhmn 19 | ` 20 | ) 21 | 22 | const ( 23 | EntryTypeCode = "code" 24 | EntryTypeSolution = "solution" 25 | EntryTypeAll = "" 26 | ) 27 | 28 | //Store defines the store.yaml file structure 29 | type Store struct { 30 | Scripts []Script `json:"scripts,omitempty" yaml:"scripts,omitempty"` 31 | } 32 | 33 | var sensitiveWords = []string{"key", 34 | "secret", 35 | "auth", 36 | "creds", 37 | "credential", 38 | "token", 39 | "bearer"} 40 | 41 | //Script defines the structure of an entry in the `Store` 42 | type Script struct { 43 | ID string `json:"id,omitempty" yaml:"id,omitempty"` 44 | CreationTime time.Time `json:"creationtime,omitempty" yaml:"creationtime,omitempty"` 45 | UpdateTime time.Time `json:"updatetime,omitempty" yaml:"updatetime,omitempty"` 46 | Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` 47 | Solution Solution `json:"solution,omitempty" yaml:"solution,omitempty"` 48 | Code Code `json:"code,omitempty" yaml:"code,omitempty"` 49 | } 50 | 51 | func getField(field, value string) string { 52 | if value != "" { 53 | return fmt.Sprintf("%s: %s\n", field, value) 54 | } 55 | return "" 56 | } 57 | 58 | func (s Script) String() string { 59 | list := "" 60 | if s.Solution.Content != "" { 61 | list += getField("ID", s.ID) 62 | list += getField("CreationTime", s.CreationTime.Format(time.RFC1123)) 63 | list += getField("UpdateTime", s.UpdateTime.Format(time.RFC1123)) 64 | list += " Type: Solution\n" 65 | list += getField(" Comment", s.Comment) 66 | list += getField(" Solution", s.Solution.Content) 67 | } else { 68 | list += getField("ID", s.ID) 69 | list += getField("CreationTime", s.CreationTime.Format(time.RFC1123)) 70 | list += getField("UpdateTime", s.UpdateTime.Format(time.RFC1123)) 71 | list += " Type: Code\n" 72 | list += getField(" Alias", s.Code.Alias) 73 | list += getField(" Comment", s.Comment) 74 | list += getField(" Code", s.Code.Content) 75 | } 76 | 77 | return list 78 | } 79 | 80 | type Solution struct { 81 | Content string `json:"content,omitempty" yaml:"content,omitempty"` 82 | } 83 | 84 | type Code struct { 85 | Content string `json:"content,omitempty" yaml:"content,omitempty"` 86 | //Alias is the alias defined for the bash script in the rc file reference 87 | Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` 88 | } 89 | 90 | //EntryAlreadyExist checks that a script entry of `id` already exist in the store 91 | func (s *Store) EntryAlreadyExist(id string) bool { 92 | scripts := s.Scripts 93 | 94 | for _, script := range scripts { 95 | if script.ID == id { 96 | return true 97 | } 98 | } 99 | 100 | return false 101 | } 102 | 103 | //SaveStore saves the `Store` struct to the `filepath` 104 | func (s *Store) SaveStore(filepath string) error { 105 | data, err := yaml.Marshal(s) 106 | if err != nil { 107 | return fmt.Errorf("failed to Marshal store struct: %s", err) 108 | } 109 | content := fmt.Sprintf("%s\n%s", autoGeneratedHeader, string(data)) 110 | return ioutil.WriteFile(filepath, []byte(content), 0666) 111 | } 112 | 113 | //LoadStore loads store struct from solution repository 114 | //Returns: 115 | // - a pointer to the `Store` struct created 116 | // - the content of the store file in `[]byte` 117 | // - an error in case of failure 118 | func LoadStore(filepath string) (*Store, []byte, error) { 119 | s := &Store{} 120 | 121 | if _, err := os.Stat(filepath); os.IsNotExist(err) { 122 | return s, []byte{}, nil 123 | } 124 | 125 | data, err := ioutil.ReadFile(filepath) 126 | if err != nil { 127 | return nil, nil, fmt.Errorf("failed to read file: %s", err) 128 | } 129 | 130 | err = yaml.Unmarshal(data, s) 131 | if err != nil { 132 | return s, data, fmt.Errorf("failed to unmarshal data: %s", err) 133 | } 134 | 135 | return s, data, nil 136 | } 137 | 138 | //GenereateIdempotentID generate a unique sha256 139 | func GenereateIdempotentID(code, comment, alias, solution string) (string, error) { 140 | id := []byte(fmt.Sprintf("%s-%s-%s-%s", code, comment, alias, solution)) 141 | hash := sha256.New() 142 | if _, err := hash.Write(id); err != nil { 143 | return "", err 144 | } 145 | return hex.EncodeToString(hash.Sum(nil)), nil 146 | } 147 | 148 | //HasSensitiveData checks if the `s` contains sensitive data 149 | // 150 | //Returns true and the keyword that was found in the string `s` 151 | //was found 152 | //Returns false when no keyword was found 153 | func HasSensitiveData(s string) (bool, string) { 154 | s = strings.ToLower(s) 155 | 156 | for _, w := range sensitiveWords { 157 | strings.Contains(s, w) 158 | if strings.Contains(s, w) { 159 | return true, w 160 | } 161 | } 162 | 163 | return false, "" 164 | } 165 | -------------------------------------------------------------------------------- /internal/printers/printers.go: -------------------------------------------------------------------------------- 1 | package printers 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/elhmn/ckp/internal/store" 9 | "github.com/manifoldco/promptui" 10 | ) 11 | 12 | const selectItemsSize = 10 13 | 14 | var defaultPrinters = Printers{} 15 | 16 | type IPrinters interface { 17 | Confirm(message string) bool 18 | SelectScriptEntry(scripts []store.Script, entryType string) (int, string, error) 19 | } 20 | 21 | type Printers struct{} 22 | 23 | //NewPrinters returns new printers struct 24 | func NewPrinters() *Printers { 25 | return &Printers{} 26 | } 27 | 28 | func (p Printers) Confirm(message string) bool { 29 | validate := func(input string) error { 30 | input = strings.ToLower(strings.TrimSpace(input)) 31 | if input != "y" && input != "n" { 32 | return fmt.Errorf("wrong input %s, was expecting `y` or `n`", input) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | msg := message + " Press (y/n)" 39 | prompt := promptui.Prompt{ 40 | Label: msg, 41 | Validate: validate, 42 | } 43 | 44 | result, err := prompt.Run() 45 | if err != nil { 46 | return false 47 | } 48 | input := strings.ToLower(strings.TrimSpace(result)) 49 | 50 | return input == "y" 51 | } 52 | 53 | //Confirm prompt a confirmation message 54 | // 55 | //Return true if the user entered Y/y and false if entered n/N 56 | func Confirm(message string) bool { 57 | return defaultPrinters.Confirm(message) 58 | } 59 | 60 | func SelectScriptEntry(scripts []store.Script, entryType string) (int, string, error) { 61 | return defaultPrinters.SelectScriptEntry(scripts, entryType) 62 | } 63 | 64 | //SelectScriptEntry prompt a search 65 | //returns the selected entry index 66 | func (p Printers) SelectScriptEntry(scripts []store.Script, entryType string) (int, string, error) { 67 | searchScript := func(input string, index int) bool { 68 | s := scripts[index] 69 | if entryType == store.EntryTypeCode { 70 | return s.Code.Content != "" && DoesScriptContain(s, input) 71 | } else if entryType == store.EntryTypeSolution { 72 | return s.Solution.Content != "" && DoesScriptContain(s, input) 73 | } 74 | 75 | return DoesScriptContain(s, input) 76 | } 77 | 78 | prompt := promptui.Select{ 79 | Label: "Enter your search text", 80 | Items: scripts, 81 | Size: selectItemsSize, 82 | StartInSearchMode: true, 83 | Searcher: searchScript, 84 | Templates: getTemplates(), 85 | } 86 | 87 | i, result, err := prompt.Run() 88 | if err != nil { 89 | return i, "", fmt.Errorf("prompt failed %v", err) 90 | } 91 | 92 | return i, result, nil 93 | } 94 | 95 | func trimText(s string) string { 96 | if len(s) > 50 { 97 | return s[:50] + "..." 98 | } 99 | return s 100 | } 101 | 102 | func getTemplates() *promptui.SelectTemplates { 103 | funcMap := promptui.FuncMap 104 | funcMap["inline"] = func(s string) string { 105 | return strings.ReplaceAll(trimText(s), "\n", " ") 106 | } 107 | 108 | //if you find a hard time understand it check out golang templating format documentation 109 | //here https://golang.org/pkg/text/template 110 | return &promptui.SelectTemplates{ 111 | Label: "{{ if .Code.Content -}} {{`code:` | bold | green}} " + 112 | "{{ inline .Code.Content}} {{- else -}} {{ inline .Solution.Content }} {{ end }}", 113 | Active: "* {{ if .Code.Content -}} {{`code:` | bold | green}} {{ inline .Code.Content | bold}} {{ else }} " + 114 | "{{`solution:` | bold | yellow }} {{ inline .Solution.Content | bold }} {{ end }}", 115 | Inactive: "{{ if .Code.Content -}} {{`code:` | green }} {{ inline .Code.Content }} " + 116 | "{{- else -}} {{`solution:` | yellow}} {{ inline .Solution.Content }} {{ end }}", 117 | Selected: " {{ `✓` | green }} {{if .Code.Content -}} {{ inline .Code.Content | bold }} {{- else -}} {{ inline .Solution.Content | bold }} {{ end }}", 118 | Details: "Type: {{- if .Code.Content }} code {{ else }} solution {{- end }}" + 119 | "{{ if .Code.Alias }} | Alias: {{ .Code.Alias }} {{- end }}" + 120 | "{{ if .Comment }} | Comment: {{ .Comment }} {{- end }}", 121 | FuncMap: funcMap, 122 | } 123 | } 124 | 125 | func extractScriptStringContent(script store.Script) string { 126 | code := strings.Replace(strings.ToLower(script.Code.Content), " ", "", -1) 127 | solution := strings.Replace(strings.ToLower(script.Solution.Content), " ", "", -1) 128 | comment := strings.Replace(strings.ToLower(script.Comment), " ", "", -1) 129 | alias := strings.Replace(strings.ToLower(script.Code.Alias), " ", "", -1) 130 | content := fmt.Sprintf("%s %s %s %s", code, solution, comment, alias) 131 | return content 132 | } 133 | 134 | //DoesScriptContain return true if the script contains the input value 135 | func DoesScriptContain(script store.Script, input string) bool { 136 | input = strings.TrimSpace(strings.ToLower(input)) 137 | 138 | //Build pattern 139 | pattern := ".*" + strings.Join(strings.Split(input, " "), ".*") 140 | 141 | matched, err := regexp.Match(pattern, []byte(extractScriptStringContent(script))) 142 | if err != nil { 143 | return false 144 | } 145 | 146 | return matched 147 | } 148 | -------------------------------------------------------------------------------- /cmd/push.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/briandowns/spinner" 9 | "github.com/elhmn/ckp/internal/config" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const ( 14 | commitAddAction = "add" 15 | commitEditAction = "edit" 16 | commitRemoveAction = "rm" 17 | commitDefaultAction = "push" 18 | ) 19 | 20 | //NewPushCommand create new cobra command for the push command 21 | func NewPushCommand(conf config.Config) *cobra.Command { 22 | command := &cobra.Command{ 23 | Use: "push", 24 | Short: "pushes your changes to your remote repository", 25 | Long: `pushes your changed to your remote repository 26 | 27 | example: ckp push 28 | `, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | if err := pushCommand(conf); err != nil { 31 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 32 | return 33 | } 34 | }, 35 | } 36 | 37 | return command 38 | } 39 | 40 | func pushCommand(conf config.Config) error { 41 | //Setup spinner 42 | spin := spinner.New(spinner.CharSets[11], 100*time.Millisecond) 43 | spin.Start() 44 | defer spin.Stop() 45 | 46 | dir, err := config.GetStoreDirPath(conf) 47 | if err != nil { 48 | return fmt.Errorf("failed get repository path: %s", err) 49 | } 50 | 51 | storeFilePath, err := config.GetStoreFilePath(conf) 52 | if err != nil { 53 | return fmt.Errorf("failed get store file path: %s", err) 54 | } 55 | 56 | historyStoreFilePath, err := config.GetHistoryFilePath(conf) 57 | if err != nil { 58 | return fmt.Errorf("failed get history store file path: %s", err) 59 | } 60 | 61 | spin.Suffix = " pulling remote changes..." 62 | err = pullRemoteChanges(conf, dir, storeFilePath, historyStoreFilePath) 63 | if err != nil { 64 | return fmt.Errorf("failed to pull remote changes: %s", err) 65 | } 66 | spin.Suffix = " remote changes pulled" 67 | 68 | spin.Suffix = " pushing local changes..." 69 | err = pushLocalChanges(conf, dir, commitDefaultAction, storeFilePath, historyStoreFilePath) 70 | if err != nil { 71 | return fmt.Errorf("failed to push local changes: %s", err) 72 | } 73 | spin.Suffix = " local changes pushed" 74 | 75 | fmt.Fprintf(conf.OutWriter, "\nckp store was pushed successfully\n") 76 | return nil 77 | } 78 | 79 | func pullRemoteChanges(conf config.Config, dir string, files ...string) error { 80 | hasChanges := false 81 | hasStashed := false 82 | 83 | out, err := conf.Exec.DoGit(dir, "fetch", "origin", "main") 84 | if err != nil { 85 | return fmt.Errorf("failed to fetch origin/main: %s: %s", err, out) 86 | } 87 | 88 | args := append([]string{"diff", "origin/main", "--"}, files...) 89 | out, err = conf.Exec.DoGit(dir, args...) 90 | if err != nil { 91 | return fmt.Errorf("failed to check for local changes: %s: %s", err, out) 92 | } 93 | 94 | if out != "" { 95 | hasChanges = true 96 | } 97 | 98 | if hasChanges { 99 | out, err = conf.Exec.DoGit(dir, "stash") 100 | if err != nil { 101 | return fmt.Errorf("failed to stash changes: %s: %s", err, out) 102 | } 103 | 104 | if !strings.Contains(out, "No local changes to save") { 105 | hasStashed = true 106 | } 107 | } 108 | 109 | out, err = conf.Exec.DoGit(dir, "pull", "--rebase", "origin", "main") 110 | if err != nil { 111 | return fmt.Errorf("failed to pull remote changes: %s: %s", err, out) 112 | } 113 | 114 | if hasStashed { 115 | out, err = conf.Exec.DoGit(dir, "stash", "apply") 116 | //if there is an error and that the error is not related 117 | if err != nil && !strings.Contains(out, "No stash entries found") { 118 | return fmt.Errorf("failed to apply stash changes: %s: %s", err, out) 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | func pushLocalChanges(conf config.Config, dir, action string, files ...string) error { 125 | out, err := conf.Exec.DoGit(dir, "fetch", "origin", "main") 126 | if err != nil { 127 | return fmt.Errorf("failed to fetch origin/main: %s: %s", err, out) 128 | } 129 | 130 | args := append([]string{"diff", "origin/main", "--"}, files...) 131 | out, err = conf.Exec.DoGit(dir, args...) 132 | if err != nil { 133 | return fmt.Errorf("failed to check for local changes: %s: %s", err, out) 134 | } 135 | //abort if `file` does not have changes 136 | if out == "" { 137 | return nil 138 | } 139 | 140 | args = append([]string{"add"}, files...) 141 | out, err = conf.Exec.DoGit(dir, args...) 142 | if err != nil { 143 | return fmt.Errorf("failed to add changes: %s: %s", err, out) 144 | } 145 | 146 | out, err = conf.Exec.DoGit(dir, "commit", "-m", getCommitMessage(action)) 147 | if err != nil { 148 | return fmt.Errorf("failed to commit changes: %s: %s", err, out) 149 | } 150 | 151 | out, err = conf.Exec.DoGitPush(dir, "origin", "main") 152 | if err != nil { 153 | return fmt.Errorf("failed to push store: %s: %s", err, out) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func getCommitMessage(action string) string { 160 | switch action { 161 | case commitAddAction: 162 | return "ckp: add entry" 163 | case commitEditAction: 164 | return "ckp: edit entry" 165 | case commitRemoveAction: 166 | return "ckp: remove entry" 167 | } 168 | return "update: update store" 169 | } 170 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/elhmn/ckp/internal/config" 10 | "github.com/elhmn/ckp/internal/store" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | //NewListCommand stores everything that written after --code or --solution flag 15 | func NewListCommand(conf config.Config) *cobra.Command { 16 | command := &cobra.Command{ 17 | Use: "list", 18 | Aliases: []string{"l"}, 19 | Short: "will display your code snippets and solutions", 20 | Long: `will display the code snippets and solutions you have stored 21 | 22 | example: ckp list 23 | Will list your first 10 code snippets and solutions 24 | 25 | example: ckp list --limit 20 26 | Will list your first 20 code snippets and solutions 27 | 28 | example: ckp list --from-history 29 | Will list your first 20 code snippets and solutions 30 | 31 | example: ckp list --all 32 | Will list all your code snippets and solutions 33 | 34 | example: ckp list --code 35 | Will list your first 10 code snippets only 36 | 37 | example: ckp list --solution 38 | Will list your 10 first solutions only 39 | `, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | if err := listCommand(cmd, args, conf); err != nil { 42 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 43 | return 44 | } 45 | }, 46 | } 47 | 48 | command.PersistentFlags().IntP("limit", "l", 10, `limit the number of element listed`) 49 | command.PersistentFlags().BoolP("code", "c", false, `list your code records only`) 50 | command.PersistentFlags().BoolP("solution", "s", false, `list your solutions only`) 51 | command.PersistentFlags().BoolP("all", "a", false, `list all your code and solutions`) 52 | command.PersistentFlags().Bool("from-history", false, `list code and solution records from history`) 53 | 54 | return command 55 | } 56 | 57 | func listCommand(cmd *cobra.Command, args []string, conf config.Config) error { 58 | if err := cmd.Flags().Parse(args); err != nil { 59 | return err 60 | } 61 | 62 | flags := cmd.Flags() 63 | 64 | //Get data from flags 65 | limit, err := flags.GetInt("limit") 66 | if err != nil { 67 | return fmt.Errorf("could not parse `limit` flag: %s", err) 68 | } 69 | code, err := flags.GetBool("code") 70 | if err != nil { 71 | return fmt.Errorf("could not parse `code` flag: %s", err) 72 | } 73 | solution, err := flags.GetBool("solution") 74 | if err != nil { 75 | return fmt.Errorf("could not parse `solution` flag: %s", err) 76 | } 77 | all, err := flags.GetBool("all") 78 | if err != nil { 79 | return fmt.Errorf("could not parse `all` flag: %s", err) 80 | } 81 | fromHistory, err := flags.GetBool("from-history") 82 | if err != nil { 83 | return fmt.Errorf("could not parse `fromHistory` flag: %s", err) 84 | } 85 | 86 | //get store data 87 | var storeFile string 88 | if !fromHistory { 89 | storeFile, err = config.GetStoreFilePath(conf) 90 | if err != nil { 91 | return fmt.Errorf("failed to get the store file path: %s", err) 92 | } 93 | } else { 94 | storeFile, err = config.GetHistoryFilePath(conf) 95 | if err != nil { 96 | return fmt.Errorf("failed to get the history store file path: %s", err) 97 | } 98 | } 99 | 100 | storeData, _, err := store.LoadStore(storeFile) 101 | if err != nil { 102 | return fmt.Errorf("failed to laod store: %s", err) 103 | } 104 | 105 | list := listScripts(storeData.Scripts, code, solution, all, limit) 106 | 107 | fmt.Fprintln(conf.OutWriter, list) 108 | return nil 109 | } 110 | 111 | func getField(field, value string) string { 112 | if value != "" { 113 | return fmt.Sprintf("%s: %s\n", field, value) 114 | } 115 | return "" 116 | } 117 | 118 | func sprintScript(wg *sync.WaitGroup, output []string, index int, s store.Script, isCode, isSolution bool) { 119 | defer wg.Done() 120 | 121 | list := "" 122 | //if the script is a solution 123 | if s.Solution.Content != "" { 124 | if isCode { 125 | return 126 | } 127 | list += getField("ID", s.ID) 128 | list += getField("CreationTime", s.CreationTime.Format(time.RFC1123)) 129 | list += getField("UpdateTime", s.UpdateTime.Format(time.RFC1123)) 130 | list += " Type: Solution\n" 131 | list += getField(" Comment", s.Comment) 132 | list += getField(" Solution", s.Solution.Content) 133 | } else { 134 | if isSolution { 135 | return 136 | } 137 | list += getField("ID", s.ID) 138 | list += getField("CreationTime", s.CreationTime.Format(time.RFC1123)) 139 | list += getField("UpdateTime", s.UpdateTime.Format(time.RFC1123)) 140 | list += " Type: Code\n" 141 | list += getField(" Alias", s.Code.Alias) 142 | list += getField(" Comment", s.Comment) 143 | list += getField(" Code", s.Code.Content) 144 | } 145 | list += "\n" 146 | 147 | output[index] = list 148 | } 149 | 150 | func listScripts(scripts []store.Script, isCode, isSolution, shouldListAll bool, limit int) string { 151 | size := len(scripts) 152 | wg := sync.WaitGroup{} 153 | 154 | //if --all was specified set the limit to the size of the list of scripts 155 | if shouldListAll { 156 | limit = size 157 | } 158 | 159 | output := make([]string, limit) 160 | 161 | //Buffer channel 162 | for i := 0; i < limit && i < size; i++ { 163 | wg.Add(1) 164 | s := scripts[i] 165 | go sprintScript(&wg, output, i, s, isCode, isSolution) 166 | } 167 | wg.Wait() 168 | 169 | return strings.Join(output, "") 170 | } 171 | -------------------------------------------------------------------------------- /cmd/add_history.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/elhmn/ckp/internal/config" 9 | "github.com/elhmn/ckp/internal/history" 10 | "github.com/elhmn/ckp/internal/printers" 11 | "github.com/elhmn/ckp/internal/store" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | //NewAddHistoryCommand adds everything that written after --code or --solution flag 16 | func NewAddHistoryCommand(conf config.Config) *cobra.Command { 17 | command := &cobra.Command{ 18 | Use: "history", 19 | Aliases: []string{"h"}, 20 | Short: "will store code from your shell history", 21 | Long: `will store code from your shell history 22 | it will read your .bash_history and zsh_history files and store 23 | every script oneliner as a code entry in your store.yaml file 24 | 25 | example: ckp history 26 | `, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | if err := addHistoryCommand(cmd, args, conf); err != nil { 29 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 30 | return 31 | } 32 | }, 33 | } 34 | 35 | command.PersistentFlags().BoolP("skip-secrets", "s", false, `will skip secrets and sensitive informations`) 36 | 37 | return command 38 | } 39 | 40 | func addHistoryCommand(cmd *cobra.Command, args []string, conf config.Config) error { 41 | if err := cmd.Flags().Parse(args); err != nil { 42 | return err 43 | } 44 | 45 | flags := cmd.Flags() 46 | 47 | //Get data from flags 48 | shouldSkipSecrets, err := flags.GetBool("skip-secrets") 49 | if err != nil { 50 | return fmt.Errorf("could not parse `--skip-secrets` flag: %s", err) 51 | } 52 | 53 | historyStoreFilePath, err := config.GetHistoryFilePath(conf) 54 | if err != nil { 55 | return fmt.Errorf("failed get store file path: %s", err) 56 | } 57 | 58 | _, storeData, storeBytes, err := loadStore(historyStoreFilePath) 59 | if err != nil { 60 | return fmt.Errorf("failed to load the store: %s", err) 61 | } 62 | 63 | tempFile, err := createHistoryTempFile(conf, storeBytes) 64 | if err != nil { 65 | return fmt.Errorf("failed to create tempFile: %s", err) 66 | } 67 | 68 | records, err := history.GetHistoryRecords() 69 | if err != nil { 70 | return fmt.Errorf("failed to get history records: %s", err) 71 | } 72 | 73 | //Setup spinner 74 | conf.Spin.Start() 75 | defer conf.Spin.Stop() 76 | 77 | dir, err := config.GetStoreDirPath(conf) 78 | if err != nil { 79 | return fmt.Errorf("failed get repository path: %s", err) 80 | } 81 | 82 | conf.Spin.Message("pulling remote changes...") 83 | err = pullRemoteChanges(conf, dir, historyStoreFilePath) 84 | if err != nil { 85 | return fmt.Errorf("failed to pull remote changes: %s", err) 86 | } 87 | conf.Spin.Message("remote changes pulled") 88 | 89 | //Add history code records 90 | addScriptsFromRecords(conf, records, storeData, shouldSkipSecrets) 91 | 92 | //Save storeData in store 93 | if err := saveStore(storeData, storeBytes, historyStoreFilePath, tempFile); err != nil { 94 | return fmt.Errorf("failed to save store in %s: %s", historyStoreFilePath, err) 95 | } 96 | 97 | //Delete the temporary file 98 | if err := os.RemoveAll(tempFile); err != nil { 99 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 100 | } 101 | 102 | conf.Spin.Message("pushing local changes...") 103 | err = pushLocalChanges(conf, dir, commitAddAction, historyStoreFilePath) 104 | if err != nil { 105 | return fmt.Errorf("failed to push local changes: %s", err) 106 | } 107 | conf.Spin.Message("local changes pushed") 108 | 109 | fmt.Fprintln(conf.OutWriter, "\nYour history was successfully added!") 110 | return nil 111 | } 112 | 113 | func addScriptsFromRecords(conf config.Config, records []string, storeData *store.Store, shouldSkipSecrets bool) *store.Store { 114 | size := len(records) 115 | for i, record := range records { 116 | conf.Spin.Message(fmt.Sprintf("%d/%d adding record...", i, size)) 117 | 118 | //Check if the code entry contains sensitive data 119 | if ret, word := store.HasSensitiveData(record); ret { 120 | if shouldSkipSecrets { 121 | continue 122 | } 123 | 124 | fmt.Fprintf(conf.OutWriter, "Found the keyword `%s` in %s\n", word, record) 125 | fmt.Fprintf(conf.OutWriter, "%d/%d records\n", i, size) 126 | if !printers.Confirm("Add anyway ?") { 127 | fmt.Fprintf(conf.OutWriter, "Code entry addition was aborted!\n") 128 | continue 129 | } 130 | } 131 | 132 | //Read history file parse its content and store each entry 133 | script, err := createNewHistoryScriptEntry(record) 134 | if err != nil { 135 | fmt.Fprintf(conf.ErrWriter, "failed to create new script entry: %s", err) 136 | continue 137 | } 138 | 139 | if storeData.EntryAlreadyExist(script.ID) { 140 | continue 141 | } 142 | 143 | //Add new script entry in the `Store` struct 144 | storeData.Scripts = append(storeData.Scripts, script) 145 | 146 | conf.Spin.Message(fmt.Sprintf("%d/%d record successfully added!", i, size)) 147 | } 148 | 149 | return storeData 150 | } 151 | 152 | //createNewHistoryScriptEntry return a new code Script entry 153 | func createNewHistoryScriptEntry(code string) (store.Script, error) { 154 | timeNow := time.Now() 155 | //Generate script entry unique id 156 | id, err := store.GenereateIdempotentID(code, "", "", "") 157 | if err != nil { 158 | return store.Script{}, fmt.Errorf("failed to generate idem potent id: %s", err) 159 | } 160 | 161 | return store.Script{ 162 | ID: id, 163 | CreationTime: timeNow, 164 | UpdateTime: timeNow, 165 | Code: store.Code{ 166 | Content: code, 167 | }, 168 | }, nil 169 | } 170 | -------------------------------------------------------------------------------- /cmd/add_code_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/elhmn/ckp/cmd" 10 | "github.com/elhmn/ckp/internal/config" 11 | "github.com/elhmn/ckp/internal/files" 12 | "github.com/elhmn/ckp/mocks" 13 | "github.com/golang/mock/gomock" 14 | "github.com/mitchellh/go-homedir" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func createConfig(t *testing.T) config.Config { 19 | mockCtrl := gomock.NewController(t) 20 | defer mockCtrl.Finish() 21 | 22 | conf := config.NewDefaultConfig(config.Options{Version: "0.0.0+dev"}) 23 | conf.Exec = mocks.NewMockIExec(mockCtrl) 24 | conf.Printers = mocks.NewMockIPrinters(mockCtrl) 25 | 26 | //Think of deleting this file later on 27 | conf.CKPDir = ".ckp_test" 28 | return conf 29 | } 30 | 31 | func getTempStorageFolder(conf config.Config) (string, error) { 32 | home, err := homedir.Dir() 33 | if err != nil { 34 | return "", fmt.Errorf("failed to read home directory: %s", err) 35 | } 36 | 37 | return fmt.Sprintf("%s/%s/%s", home, conf.CKPDir, conf.CKPStorageFolder), nil 38 | } 39 | 40 | func setupFolder(conf config.Config) error { 41 | if err := deleteFolder(conf); err != nil { 42 | return fmt.Errorf("Error: failed to delete folder: %s", err) 43 | } 44 | 45 | folder, err := getTempStorageFolder(conf) 46 | if err != nil { 47 | return fmt.Errorf("failed to get temporary storage folder: %s", err) 48 | } 49 | 50 | if err = os.MkdirAll(folder, 0777); err != nil { 51 | return err 52 | } 53 | 54 | if err = files.CopyFileToHomeDirectory(conf.CKPDir+"/"+config.StoreFileName, "../fixtures/store.yaml"); err != nil { 55 | return err 56 | } 57 | 58 | if err = files.CopyFileToHomeDirectory(conf.CKPDir+"/"+config.HistoryFileName, "../fixtures/2mb_store.yaml"); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func deleteFolder(conf config.Config) error { 66 | home, err := homedir.Dir() 67 | if err != nil { 68 | return fmt.Errorf("failed to read home directory: %s", err) 69 | } 70 | 71 | return os.RemoveAll(fmt.Sprintf("%s/%s", home, conf.CKPDir)) 72 | } 73 | 74 | func TestAddCodeCommand(t *testing.T) { 75 | t.Run("make sure that it runs successfully", func(t *testing.T) { 76 | conf := createConfig(t) 77 | mockedExec := conf.Exec.(*mocks.MockIExec) 78 | writer := &bytes.Buffer{} 79 | conf.OutWriter = writer 80 | 81 | if err := setupFolder(conf); err != nil { 82 | t.Errorf("Error: failed with %s", err) 83 | } 84 | 85 | //Specify expectations 86 | gomock.InOrder( 87 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 88 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 89 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 90 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 91 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 92 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 93 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 94 | ) 95 | 96 | commandName := "code" 97 | command := cmd.NewAddCommand(conf) 98 | //Set writer 99 | command.SetOutput(conf.OutWriter) 100 | 101 | //Set args 102 | command.SetArgs([]string{commandName, 103 | "echo \"je suis con\"", 104 | "--comment", "a_comment", 105 | "--alias", "an_alias", 106 | }) 107 | 108 | err := command.Execute() 109 | if err != nil { 110 | t.Errorf("Error: failed with %s", err) 111 | } 112 | 113 | got := writer.String() 114 | exp := "\nYour code was successfully added!\n" 115 | assert.Contains(t, got, exp) 116 | 117 | //function call assert 118 | if err := deleteFolder(conf); err != nil { 119 | t.Errorf("Error: failed with %s", err) 120 | } 121 | }) 122 | 123 | t.Run("make sure that it runs successfully without code arguument", func(t *testing.T) { 124 | conf := createConfig(t) 125 | mockedExec := conf.Exec.(*mocks.MockIExec) 126 | writer := &bytes.Buffer{} 127 | conf.OutWriter = writer 128 | 129 | if err := setupFolder(conf); err != nil { 130 | t.Errorf("Error: failed with %s", err) 131 | } 132 | 133 | //Specify expectations 134 | gomock.InOrder( 135 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 136 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 137 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 138 | mockedExec.EXPECT().OpenEditor(gomock.Any(), gomock.Any()).Return(nil), 139 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 140 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 141 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 142 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 143 | ) 144 | 145 | commandName := "code" 146 | command := cmd.NewAddCommand(conf) 147 | //Set writer 148 | command.SetOutput(conf.OutWriter) 149 | 150 | //Set args 151 | command.SetArgs([]string{commandName, 152 | "--comment", "a_comment", 153 | "--alias", "an_alias", 154 | }) 155 | 156 | err := command.Execute() 157 | if err != nil { 158 | t.Errorf("Error: failed with %s", err) 159 | } 160 | 161 | got := writer.String() 162 | exp := "\nYour code was successfully added!\n" 163 | assert.Contains(t, got, exp) 164 | 165 | //function call assert 166 | if err := deleteFolder(conf); err != nil { 167 | t.Errorf("Error: failed with %s", err) 168 | } 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /mocks/IExec.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/exec/exec.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockIExec is a mock of IExec interface. 14 | type MockIExec struct { 15 | ctrl *gomock.Controller 16 | recorder *MockIExecMockRecorder 17 | } 18 | 19 | // MockIExecMockRecorder is the mock recorder for MockIExec. 20 | type MockIExecMockRecorder struct { 21 | mock *MockIExec 22 | } 23 | 24 | // NewMockIExec creates a new mock instance. 25 | func NewMockIExec(ctrl *gomock.Controller) *MockIExec { 26 | mock := &MockIExec{ctrl: ctrl} 27 | mock.recorder = &MockIExecMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockIExec) EXPECT() *MockIExecMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // CreateFolderIfDoesNotExist mocks base method. 37 | func (m *MockIExec) CreateFolderIfDoesNotExist(dir string) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "CreateFolderIfDoesNotExist", dir) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // CreateFolderIfDoesNotExist indicates an expected call of CreateFolderIfDoesNotExist. 45 | func (mr *MockIExecMockRecorder) CreateFolderIfDoesNotExist(dir interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFolderIfDoesNotExist", reflect.TypeOf((*MockIExec)(nil).CreateFolderIfDoesNotExist), dir) 48 | } 49 | 50 | // DoGit mocks base method. 51 | func (m *MockIExec) DoGit(dir string, args ...string) (string, error) { 52 | m.ctrl.T.Helper() 53 | varargs := []interface{}{dir} 54 | for _, a := range args { 55 | varargs = append(varargs, a) 56 | } 57 | ret := m.ctrl.Call(m, "DoGit", varargs...) 58 | ret0, _ := ret[0].(string) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // DoGit indicates an expected call of DoGit. 64 | func (mr *MockIExecMockRecorder) DoGit(dir interface{}, args ...interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | varargs := append([]interface{}{dir}, args...) 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoGit", reflect.TypeOf((*MockIExec)(nil).DoGit), varargs...) 68 | } 69 | 70 | // DoGitClone mocks base method. 71 | func (m *MockIExec) DoGitClone(dir string, args ...string) (string, error) { 72 | m.ctrl.T.Helper() 73 | varargs := []interface{}{dir} 74 | for _, a := range args { 75 | varargs = append(varargs, a) 76 | } 77 | ret := m.ctrl.Call(m, "DoGitClone", varargs...) 78 | ret0, _ := ret[0].(string) 79 | ret1, _ := ret[1].(error) 80 | return ret0, ret1 81 | } 82 | 83 | // DoGitClone indicates an expected call of DoGitClone. 84 | func (mr *MockIExecMockRecorder) DoGitClone(dir interface{}, args ...interface{}) *gomock.Call { 85 | mr.mock.ctrl.T.Helper() 86 | varargs := append([]interface{}{dir}, args...) 87 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoGitClone", reflect.TypeOf((*MockIExec)(nil).DoGitClone), varargs...) 88 | } 89 | 90 | // DoGitPush mocks base method. 91 | func (m *MockIExec) DoGitPush(dir string, args ...string) (string, error) { 92 | m.ctrl.T.Helper() 93 | varargs := []interface{}{dir} 94 | for _, a := range args { 95 | varargs = append(varargs, a) 96 | } 97 | ret := m.ctrl.Call(m, "DoGitPush", varargs...) 98 | ret0, _ := ret[0].(string) 99 | ret1, _ := ret[1].(error) 100 | return ret0, ret1 101 | } 102 | 103 | // DoGitPush indicates an expected call of DoGitPush. 104 | func (mr *MockIExecMockRecorder) DoGitPush(dir interface{}, args ...interface{}) *gomock.Call { 105 | mr.mock.ctrl.T.Helper() 106 | varargs := append([]interface{}{dir}, args...) 107 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoGitPush", reflect.TypeOf((*MockIExec)(nil).DoGitPush), varargs...) 108 | } 109 | 110 | // OpenEditor mocks base method. 111 | func (m *MockIExec) OpenEditor(editor string, args ...string) error { 112 | m.ctrl.T.Helper() 113 | varargs := []interface{}{editor} 114 | for _, a := range args { 115 | varargs = append(varargs, a) 116 | } 117 | ret := m.ctrl.Call(m, "OpenEditor", varargs...) 118 | ret0, _ := ret[0].(error) 119 | return ret0 120 | } 121 | 122 | // OpenEditor indicates an expected call of OpenEditor. 123 | func (mr *MockIExecMockRecorder) OpenEditor(editor interface{}, args ...interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | varargs := append([]interface{}{editor}, args...) 126 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenEditor", reflect.TypeOf((*MockIExec)(nil).OpenEditor), varargs...) 127 | } 128 | 129 | // Run mocks base method. 130 | func (m *MockIExec) Run(dir, command string, args ...string) ([]byte, error) { 131 | m.ctrl.T.Helper() 132 | varargs := []interface{}{dir, command} 133 | for _, a := range args { 134 | varargs = append(varargs, a) 135 | } 136 | ret := m.ctrl.Call(m, "Run", varargs...) 137 | ret0, _ := ret[0].([]byte) 138 | ret1, _ := ret[1].(error) 139 | return ret0, ret1 140 | } 141 | 142 | // Run indicates an expected call of Run. 143 | func (mr *MockIExecMockRecorder) Run(dir, command interface{}, args ...interface{}) *gomock.Call { 144 | mr.mock.ctrl.T.Helper() 145 | varargs := append([]interface{}{dir, command}, args...) 146 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockIExec)(nil).Run), varargs...) 147 | } 148 | 149 | // RunInteractive mocks base method. 150 | func (m *MockIExec) RunInteractive(command string, args ...string) error { 151 | m.ctrl.T.Helper() 152 | varargs := []interface{}{command} 153 | for _, a := range args { 154 | varargs = append(varargs, a) 155 | } 156 | ret := m.ctrl.Call(m, "RunInteractive", varargs...) 157 | ret0, _ := ret[0].(error) 158 | return ret0 159 | } 160 | 161 | // RunInteractive indicates an expected call of RunInteractive. 162 | func (mr *MockIExecMockRecorder) RunInteractive(command interface{}, args ...interface{}) *gomock.Call { 163 | mr.mock.ctrl.T.Helper() 164 | varargs := append([]interface{}{command}, args...) 165 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunInteractive", reflect.TypeOf((*MockIExec)(nil).RunInteractive), varargs...) 166 | } 167 | -------------------------------------------------------------------------------- /cmd/add_solution.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/elhmn/ckp/internal/config" 11 | "github.com/elhmn/ckp/internal/printers" 12 | "github.com/elhmn/ckp/internal/store" 13 | "github.com/spf13/cobra" 14 | flag "github.com/spf13/pflag" 15 | ) 16 | 17 | const editorFileSolutionTemplate = `## comment: %s 18 | %s 19 | ` 20 | 21 | //NewAddSolutionCommand adds everything that written after --solution or --solution flag 22 | func NewAddSolutionCommand(conf config.Config) *cobra.Command { 23 | command := &cobra.Command{ 24 | Use: "solution ", 25 | Aliases: []string{"s"}, 26 | Short: "add solution will store your solution", 27 | Long: `add solution will store your solution in your solution repository 28 | 29 | example: ckp add solution 'echo this is my command' 30 | Will store 'echo this is my command' as a solution asset in your solution repository 31 | `, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | if err := addSolutionCommand(cmd, args, conf); err != nil { 34 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 35 | return 36 | } 37 | }, 38 | } 39 | 40 | command.PersistentFlags().StringP("path", "p", "", `add code from path`) 41 | 42 | return command 43 | } 44 | 45 | func addSolutionCommand(cmd *cobra.Command, args []string, conf config.Config) error { 46 | if err := cmd.Flags().Parse(args); err != nil { 47 | return err 48 | } 49 | flags := cmd.Flags() 50 | solution := strings.Join(args, " ") 51 | 52 | //Check if the code entry contains sensitive data 53 | if ret, word := store.HasSensitiveData(solution); ret { 54 | fmt.Fprintf(conf.OutWriter, "Found the keyword `%s` in %s\n", word, solution) 55 | if !printers.Confirm("Add anyway ?") { 56 | fmt.Fprintf(conf.OutWriter, "Solution entry addition was aborted!\n") 57 | return nil 58 | } 59 | } 60 | 61 | //Setup spinner 62 | conf.Spin.Start() 63 | defer conf.Spin.Stop() 64 | 65 | dir, err := config.GetStoreDirPath(conf) 66 | if err != nil { 67 | return fmt.Errorf("failed get repository path: %s", err) 68 | } 69 | 70 | storeFilePath, err := config.GetStoreFilePath(conf) 71 | if err != nil { 72 | return fmt.Errorf("failed get store file path: %s", err) 73 | } 74 | 75 | conf.Spin.Message(" pulling remote changes...") 76 | err = pullRemoteChanges(conf, dir, storeFilePath) 77 | if err != nil { 78 | return fmt.Errorf("failed to pull remote changes: %s", err) 79 | } 80 | conf.Spin.Message(" remote changes pulled") 81 | 82 | conf.Spin.Message(" adding new solution entry...") 83 | _, storeData, storeBytes, err := loadStore(storeFilePath) 84 | if err != nil { 85 | return fmt.Errorf("failed to load the store: %s", err) 86 | } 87 | 88 | tempFile, err := createTempFile(conf, storeBytes) 89 | if err != nil { 90 | return fmt.Errorf("failed to create tempFile: %s", err) 91 | } 92 | 93 | script, err := createNewSolutionScriptEntry(solution, flags) 94 | if err != nil { 95 | return fmt.Errorf("failed to create new script entry: %s", err) 96 | } 97 | 98 | //if it is an interactive update 99 | if len(args) == 0 { 100 | conf.Spin.Stop() 101 | s, err := getNewEntryDataFromFile(conf, script, SolutionEntryTemplateType) 102 | if err != nil { 103 | return fmt.Errorf("failed to get new entry from the editor %s", err) 104 | } 105 | 106 | //Generate an ID for the newly added script 107 | s.ID, err = store.GenereateIdempotentID("", s.Comment, "", s.Solution.Content) 108 | if err != nil { 109 | return fmt.Errorf("failed to generate idem potent ID: %s", err) 110 | } 111 | 112 | script.ID = s.ID 113 | script.Solution = s.Solution 114 | script.Comment = s.Comment 115 | conf.Spin.Start() 116 | } 117 | 118 | if storeData.EntryAlreadyExist(script.ID) { 119 | //Delete the temporary file 120 | if err := os.RemoveAll(tempFile); err != nil { 121 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 122 | } 123 | 124 | return fmt.Errorf("An identical record was found in the storage, please try `ckp edit %s`", script.ID) 125 | } 126 | 127 | //Add new script entry in the `Store` struct 128 | storeData.Scripts = append(storeData.Scripts, script) 129 | 130 | //Save storeData in store 131 | if err := saveStore(storeData, storeBytes, storeFilePath, tempFile); err != nil { 132 | return fmt.Errorf("failed to save store in %s: %s", storeFilePath, err) 133 | } 134 | 135 | //Delete the temporary file 136 | if err := os.RemoveAll(tempFile); err != nil { 137 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 138 | } 139 | conf.Spin.Message(" new entry successfully added") 140 | 141 | conf.Spin.Message(" pushing local changes...") 142 | err = pushLocalChanges(conf, dir, commitAddAction, storeFilePath) 143 | if err != nil { 144 | return fmt.Errorf("failed to push local changes: %s", err) 145 | } 146 | conf.Spin.Message(" local changes pushed") 147 | 148 | fmt.Fprintln(conf.OutWriter, "\nYour solution was successfully added!") 149 | fmt.Fprintf(conf.OutWriter, "\n%s", script) 150 | return nil 151 | } 152 | 153 | //createNewSolutionScriptEntry return a new solution entry 154 | func createNewSolutionScriptEntry(solution string, flags *flag.FlagSet) (store.Script, error) { 155 | timeNow := time.Now() 156 | 157 | comment, err := flags.GetString("comment") 158 | if err != nil { 159 | return store.Script{}, fmt.Errorf("could not parse `comment` flag: %s", err) 160 | } 161 | path, err := flags.GetString("path") 162 | if err != nil { 163 | return store.Script{}, fmt.Errorf("could not parse `path` flag: %s", err) 164 | } 165 | 166 | if path != "" { 167 | bytes, err := ioutil.ReadFile(path) 168 | if err != nil { 169 | return store.Script{}, fmt.Errorf("failed to read %s: %s", path, err) 170 | } 171 | solution = string(bytes) 172 | } 173 | 174 | //Generate script entry unique id 175 | id, err := store.GenereateIdempotentID("", comment, "", solution) 176 | if err != nil { 177 | return store.Script{}, fmt.Errorf("failed to generate idem potent id: %s", err) 178 | } 179 | 180 | return store.Script{ 181 | ID: id, 182 | Comment: comment, 183 | CreationTime: timeNow, 184 | UpdateTime: timeNow, 185 | Solution: store.Solution{ 186 | Content: solution, 187 | }, 188 | }, nil 189 | } 190 | -------------------------------------------------------------------------------- /cmd/edit_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/cmd" 8 | "github.com/elhmn/ckp/internal/config" 9 | "github.com/elhmn/ckp/internal/store" 10 | "github.com/elhmn/ckp/mocks" 11 | "github.com/golang/mock/gomock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestEditCommand(t *testing.T) { 16 | t.Run("make sure that it runs successfully for code edition", func(t *testing.T) { 17 | conf := createConfig(t) 18 | mockedExec := conf.Exec.(*mocks.MockIExec) 19 | mockedPrinters := conf.Printers.(*mocks.MockIPrinters) 20 | writer := &bytes.Buffer{} 21 | conf.OutWriter = writer 22 | 23 | if err := setupFolder(conf); err != nil { 24 | t.Errorf("Error: failed with %s", err) 25 | } 26 | 27 | //Specify expectations 28 | gomock.InOrder( 29 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 30 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 31 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 32 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 33 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 34 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 35 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 36 | mockedPrinters.EXPECT().SelectScriptEntry(gomock.Any(), store.EntryTypeAll).Return(0, "", nil), 37 | ) 38 | 39 | command := cmd.NewEditCommand(conf) 40 | //Set writer 41 | command.SetOutput(conf.OutWriter) 42 | 43 | //Set args 44 | codeID := "hash-of-file-content" 45 | command.SetArgs([]string{codeID, 46 | "--code", "a_code", 47 | "--comment", "a_comment", 48 | "--alias", "an_alias", 49 | }) 50 | 51 | err := command.Execute() 52 | if err != nil { 53 | t.Errorf("Error: failed with %s", err) 54 | } 55 | 56 | got := writer.String() 57 | exp := "\nYour entry was successfully edited!\n" 58 | assert.Contains(t, got, exp) 59 | 60 | //Test that the store was correctly edited 61 | filePath, _ := config.GetStoreFilePath(conf) 62 | s, _, err := store.LoadStore(filePath) 63 | if err != nil { 64 | t.Errorf("Error: failed to load the store with %s", err) 65 | } 66 | script := s.Scripts[1] 67 | assert.Equal(t, "a_code", script.Code.Content) 68 | assert.Equal(t, "a_comment", script.Comment) 69 | assert.Equal(t, "an_alias", script.Code.Alias) 70 | 71 | //function call assert 72 | if err := deleteFolder(conf); err != nil { 73 | t.Errorf("Error: failed with %s", err) 74 | } 75 | }) 76 | 77 | t.Run("make sure that it runs successfully for solution edition", func(t *testing.T) { 78 | conf := createConfig(t) 79 | mockedExec := conf.Exec.(*mocks.MockIExec) 80 | mockedPrinters := conf.Printers.(*mocks.MockIPrinters) 81 | writer := &bytes.Buffer{} 82 | conf.OutWriter = writer 83 | 84 | if err := setupFolder(conf); err != nil { 85 | t.Errorf("Error: failed with %s", err) 86 | } 87 | 88 | //Specify expectations 89 | gomock.InOrder( 90 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 91 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 92 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 93 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 94 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 95 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 96 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 97 | mockedPrinters.EXPECT().SelectScriptEntry(gomock.Any(), store.EntryTypeAll).Return(1, "", nil), 98 | ) 99 | 100 | command := cmd.NewEditCommand(conf) 101 | //Set writer 102 | command.SetOutput(conf.OutWriter) 103 | 104 | //Set args 105 | solutionID := "hash-of-file-content-2" 106 | command.SetArgs([]string{solutionID, 107 | "--solution", "a_solution", 108 | "--comment", "a_comment", 109 | }) 110 | 111 | err := command.Execute() 112 | if err != nil { 113 | t.Errorf("Error: failed with %s", err) 114 | } 115 | 116 | got := writer.String() 117 | exp := "\nYour entry was successfully edited!\n" 118 | assert.Contains(t, got, exp) 119 | 120 | //Test that the store was correctly edited 121 | filePath, _ := config.GetStoreFilePath(conf) 122 | s, _, err := store.LoadStore(filePath) 123 | if err != nil { 124 | t.Errorf("Error: failed to load the store with %s", err) 125 | } 126 | script := s.Scripts[1] 127 | assert.Equal(t, "a_solution", script.Solution.Content) 128 | assert.Equal(t, "a_comment", script.Comment) 129 | 130 | //function call assert 131 | if err := deleteFolder(conf); err != nil { 132 | t.Errorf("Error: failed with %s", err) 133 | } 134 | }) 135 | 136 | t.Run("make sure that it runs successfully without entryID", func(t *testing.T) { 137 | conf := createConfig(t) 138 | mockedExec := conf.Exec.(*mocks.MockIExec) 139 | mockedPrinters := conf.Printers.(*mocks.MockIPrinters) 140 | writer := &bytes.Buffer{} 141 | conf.OutWriter = writer 142 | 143 | if err := setupFolder(conf); err != nil { 144 | t.Errorf("Error: failed with %s", err) 145 | } 146 | 147 | //Specify expectations 148 | gomock.InOrder( 149 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 150 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 151 | mockedExec.EXPECT().DoGit(gomock.Any(), "pull", "--rebase", "origin", "main"), 152 | mockedPrinters.EXPECT().SelectScriptEntry(gomock.Any(), store.EntryTypeAll).Return(0, "", nil), 153 | mockedExec.EXPECT().OpenEditor(gomock.Any(), gomock.Any()).Return(nil), 154 | mockedExec.EXPECT().DoGit(gomock.Any(), "fetch", "origin", "main"), 155 | mockedExec.EXPECT().DoGit(gomock.Any(), "diff", "origin/main", "--", gomock.Any()), 156 | mockedExec.EXPECT().DoGit(gomock.Any(), "add", gomock.Any()), 157 | mockedExec.EXPECT().DoGit(gomock.Any(), "commit", "-m", "ckp: add store"), 158 | ) 159 | 160 | command := cmd.NewEditCommand(conf) 161 | //Set writer 162 | command.SetOutput(conf.OutWriter) 163 | 164 | err := command.Execute() 165 | if err != nil { 166 | t.Errorf("Error: failed with %s", err) 167 | } 168 | 169 | got := writer.String() 170 | exp := "\nYour entry was successfully edited!\n" 171 | assert.Contains(t, got, exp) 172 | 173 | //function call assert 174 | if err := deleteFolder(conf); err != nil { 175 | t.Errorf("Error: failed with %s", err) 176 | } 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /internal/store/store_test.go: -------------------------------------------------------------------------------- 1 | package store_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/elhmn/ckp/internal/store" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGenerateIdempotentID(t *testing.T) { 12 | t.Run("returns the same hash for similar content", func(t *testing.T) { 13 | id1, _ := store.GenereateIdempotentID("code", "comment", "alias", "solution") 14 | id2, _ := store.GenereateIdempotentID("code", "comment", "alias", "solution") 15 | assert.Equal(t, id1, id2) 16 | }) 17 | 18 | t.Run("returns the different hash for different content", func(t *testing.T) { 19 | id1, _ := store.GenereateIdempotentID("code", "comment", "alias", "solution") 20 | id2, _ := store.GenereateIdempotentID("code1", "comment3", "alias4", "solution5") 21 | assert.NotEqual(t, id1, id2) 22 | }) 23 | } 24 | 25 | func TestEntryAlreadyExist(t *testing.T) { 26 | t.Run("returns true when entry already exist", func(t *testing.T) { 27 | existingID := "my-id" 28 | s := store.Store{ 29 | Scripts: []store.Script{ 30 | store.Script{ID: existingID}, 31 | }, 32 | } 33 | 34 | assert.Equal(t, true, s.EntryAlreadyExist(existingID)) 35 | }) 36 | 37 | t.Run("returns false when entry does not already exist", func(t *testing.T) { 38 | nonExistingID := "my-new-id" 39 | existingID := "my-id" 40 | s := store.Store{ 41 | Scripts: []store.Script{ 42 | store.Script{ID: existingID}, 43 | }, 44 | } 45 | 46 | assert.Equal(t, false, s.EntryAlreadyExist(nonExistingID)) 47 | }) 48 | } 49 | 50 | func TestHasSensitiveDataExist(t *testing.T) { 51 | type Test struct { 52 | input string 53 | expBool bool 54 | expWord string 55 | } 56 | 57 | t.Run("Test for `key` keyword", func(t *testing.T) { 58 | tests := []Test{ 59 | {input: "je suis con", expBool: false, expWord: ""}, 60 | {input: " je suisK ey con ", expBool: false, expWord: ""}, 61 | {input: "je suis con key", expBool: true, expWord: "key"}, 62 | {input: "je suis con Key", expBool: true, expWord: "key"}, 63 | {input: "KEY je suis con", expBool: true, expWord: "key"}, 64 | {input: " je suisKey con ", expBool: true, expWord: "key"}, 65 | } 66 | 67 | for i, test := range tests { 68 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 69 | gotBool, gotWord := store.HasSensitiveData(test.input) 70 | assert.Equal(t, test.expBool, gotBool) 71 | assert.Equal(t, test.expWord, gotWord) 72 | }) 73 | } 74 | }) 75 | 76 | t.Run("Test for `secret` keyword", func(t *testing.T) { 77 | tests := []Test{ 78 | {input: " je suisSE CRET con ", expBool: false, expWord: ""}, 79 | {input: "je suis con secret", expBool: true, expWord: "secret"}, 80 | {input: "je suis con SECRET", expBool: true, expWord: "secret"}, 81 | {input: "SECRET je suis con", expBool: true, expWord: "secret"}, 82 | {input: " je suisSECRET con ", expBool: true, expWord: "secret"}, 83 | } 84 | 85 | for i, test := range tests { 86 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 87 | gotBool, gotWord := store.HasSensitiveData(test.input) 88 | assert.Equal(t, test.expBool, gotBool) 89 | assert.Equal(t, test.expWord, gotWord) 90 | }) 91 | } 92 | }) 93 | 94 | t.Run("Test for `auth` keyword", func(t *testing.T) { 95 | tests := []Test{ 96 | {input: " je suisAU TH con ", expBool: false, expWord: ""}, 97 | {input: "je suis con auth", expBool: true, expWord: "auth"}, 98 | {input: "je suis con AUTH", expBool: true, expWord: "auth"}, 99 | {input: "AUTH je suis con", expBool: true, expWord: "auth"}, 100 | {input: " je suisAuTh con ", expBool: true, expWord: "auth"}, 101 | } 102 | 103 | for i, test := range tests { 104 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 105 | gotBool, gotWord := store.HasSensitiveData(test.input) 106 | assert.Equal(t, test.expBool, gotBool) 107 | assert.Equal(t, test.expWord, gotWord) 108 | }) 109 | } 110 | }) 111 | 112 | t.Run("Test for `credential` keyword", func(t *testing.T) { 113 | tests := []Test{ 114 | {input: " je suisCR EDENTIAL con ", expBool: false, expWord: ""}, 115 | {input: "je suis con credential", expBool: true, expWord: "credential"}, 116 | {input: "je suis con CREDENTIAL", expBool: true, expWord: "credential"}, 117 | {input: "CREDENTIAL je suis con", expBool: true, expWord: "credential"}, 118 | {input: " je suisCredEntial con ", expBool: true, expWord: "credential"}, 119 | } 120 | 121 | for i, test := range tests { 122 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 123 | gotBool, gotWord := store.HasSensitiveData(test.input) 124 | assert.Equal(t, test.expBool, gotBool) 125 | assert.Equal(t, test.expWord, gotWord) 126 | }) 127 | } 128 | }) 129 | 130 | t.Run("Test for `creds` keyword", func(t *testing.T) { 131 | tests := []Test{ 132 | {input: " je suisCR EDS con ", expBool: false, expWord: ""}, 133 | {input: "je suis con creds", expBool: true, expWord: "creds"}, 134 | {input: "je suis con CREDS", expBool: true, expWord: "creds"}, 135 | {input: "CREDS je suis con", expBool: true, expWord: "creds"}, 136 | {input: " je suisCredS con ", expBool: true, expWord: "creds"}, 137 | } 138 | 139 | for i, test := range tests { 140 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 141 | gotBool, gotWord := store.HasSensitiveData(test.input) 142 | assert.Equal(t, test.expBool, gotBool) 143 | assert.Equal(t, test.expWord, gotWord) 144 | }) 145 | } 146 | }) 147 | 148 | t.Run("Test for `token` keyword", func(t *testing.T) { 149 | tests := []Test{ 150 | {input: " je suisTO KEN con ", expBool: false, expWord: ""}, 151 | {input: "je suis con token", expBool: true, expWord: "token"}, 152 | {input: "je suis con TOKEN", expBool: true, expWord: "token"}, 153 | {input: "TOKEN je suis con", expBool: true, expWord: "token"}, 154 | {input: " je suisTokEn con ", expBool: true, expWord: "token"}, 155 | } 156 | 157 | for i, test := range tests { 158 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 159 | gotBool, gotWord := store.HasSensitiveData(test.input) 160 | assert.Equal(t, test.expBool, gotBool) 161 | assert.Equal(t, test.expWord, gotWord) 162 | }) 163 | } 164 | }) 165 | 166 | t.Run("Test for `bearer` keyword", func(t *testing.T) { 167 | tests := []Test{ 168 | {input: " je suisBEA RER con ", expBool: false, expWord: ""}, 169 | {input: "je suis con bearer", expBool: true, expWord: "bearer"}, 170 | {input: "je suis con BEARER", expBool: true, expWord: "bearer"}, 171 | {input: "BEARER je suis con", expBool: true, expWord: "bearer"}, 172 | {input: " je suisBeaRer con ", expBool: true, expWord: "bearer"}, 173 | } 174 | 175 | for i, test := range tests { 176 | t.Run(fmt.Sprintf("%d-test", i), func(t *testing.T) { 177 | gotBool, gotWord := store.HasSensitiveData(test.input) 178 | assert.Equal(t, test.expBool, gotBool) 179 | assert.Equal(t, test.expWord, gotWord) 180 | }) 181 | } 182 | }) 183 | } 184 | -------------------------------------------------------------------------------- /cmd/add_code.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/elhmn/ckp/internal/config" 11 | "github.com/elhmn/ckp/internal/printers" 12 | "github.com/elhmn/ckp/internal/store" 13 | "github.com/spf13/cobra" 14 | flag "github.com/spf13/pflag" 15 | ) 16 | 17 | const editorFileCodeTemplate = `## comment: %s 18 | ## alias: %s 19 | %s 20 | ` 21 | 22 | //NewAddCodeCommand adds everything that written after --code or --solution flag 23 | func NewAddCodeCommand(conf config.Config) *cobra.Command { 24 | command := &cobra.Command{ 25 | Use: "code ", 26 | Aliases: []string{"c"}, 27 | Short: "will store your code", 28 | Long: `will store your code in your solution repository 29 | 30 | example: ckp add code 'echo this is my command' 31 | Will store 'echo this is my command' as a code asset in your solution repository 32 | 33 | example: ckp add code -p ./path_to_you_code.sh 34 | Will store the code in path_to_you_code.sh as a code asset in your solution repository 35 | `, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | if err := addCodeCommand(cmd, args, conf); err != nil { 38 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 39 | return 40 | } 41 | }, 42 | } 43 | 44 | command.PersistentFlags().StringP("alias", "a", "", `ckp add -a `) 45 | command.PersistentFlags().StringP("path", "p", "", `ckp add -p `) 46 | 47 | return command 48 | } 49 | 50 | func addCodeCommand(cmd *cobra.Command, args []string, conf config.Config) error { 51 | if err := cmd.Flags().Parse(args); err != nil { 52 | return err 53 | } 54 | flags := cmd.Flags() 55 | code := strings.Join(args, " ") 56 | 57 | //Check if the code entry contains sensitive data 58 | if ret, word := store.HasSensitiveData(code); ret { 59 | fmt.Fprintf(conf.OutWriter, "Found the keyword `%s` in %s\n", word, code) 60 | if !printers.Confirm("Add anyway ?") { 61 | fmt.Fprintf(conf.OutWriter, "Code entry addition was aborted!\n") 62 | return nil 63 | } 64 | } 65 | 66 | //Setup spinner 67 | conf.Spin.Start() 68 | defer conf.Spin.Stop() 69 | 70 | dir, err := config.GetStoreDirPath(conf) 71 | if err != nil { 72 | return fmt.Errorf("failed get repository path: %s", err) 73 | } 74 | 75 | storeFilePath, err := config.GetStoreFilePath(conf) 76 | if err != nil { 77 | return fmt.Errorf("failed get store file path: %s", err) 78 | } 79 | 80 | conf.Spin.Message(" pulling remote changes...") 81 | err = pullRemoteChanges(conf, dir, storeFilePath) 82 | if err != nil { 83 | return fmt.Errorf("failed to pull remote changes: %s", err) 84 | } 85 | conf.Spin.Message(" remote changes pulled") 86 | 87 | conf.Spin.Message(" adding new code entry...") 88 | storeFile, storeData, storeBytes, err := loadStore(storeFilePath) 89 | if err != nil { 90 | return fmt.Errorf("failed to load the store: %s", err) 91 | } 92 | 93 | tempFile, err := createTempFile(conf, storeBytes) 94 | if err != nil { 95 | return fmt.Errorf("failed to create tempFile: %s", err) 96 | } 97 | 98 | script, err := createNewCodeScriptEntry(code, flags) 99 | if err != nil { 100 | return fmt.Errorf("failed to create new script entry: %s", err) 101 | } 102 | 103 | //if it is an interactive update 104 | if len(args) == 0 { 105 | conf.Spin.Stop() 106 | s, err := getNewEntryDataFromFile(conf, script, CodeEntryTemplateType) 107 | if err != nil { 108 | return fmt.Errorf("failed to get new entry from the editor %s", err) 109 | } 110 | 111 | //Generate an ID for the newly added script 112 | s.ID, err = store.GenereateIdempotentID(s.Code.Content, s.Comment, s.Code.Alias, "") 113 | if err != nil { 114 | return fmt.Errorf("failed to generate idem potent ID: %s", err) 115 | } 116 | 117 | script.ID = s.ID 118 | script.Code = s.Code 119 | script.Comment = s.Comment 120 | conf.Spin.Start() 121 | } 122 | 123 | //Check if entry already exist 124 | if storeData.EntryAlreadyExist(script.ID) { 125 | //Delete the temporary file 126 | if err := os.RemoveAll(tempFile); err != nil { 127 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 128 | } 129 | 130 | return fmt.Errorf("An identical record was found in the storage, please try `ckp edit %s`", script.ID) 131 | } 132 | 133 | //Add new script entry in the `Store` struct 134 | storeData.Scripts = append(storeData.Scripts, script) 135 | 136 | //Save storeData in store 137 | if err := saveStore(storeData, storeBytes, storeFile, tempFile); err != nil { 138 | return fmt.Errorf("failed to save store in %s: %s", storeFile, err) 139 | } 140 | 141 | //Delete the temporary file 142 | if err := os.RemoveAll(tempFile); err != nil { 143 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 144 | } 145 | conf.Spin.Message(" new entry successfully added") 146 | 147 | //Push changes 148 | conf.Spin.Message(" pushing local changes...") 149 | err = pushLocalChanges(conf, dir, commitAddAction, storeFilePath) 150 | if err != nil { 151 | return fmt.Errorf("failed to push local changes: %s", err) 152 | } 153 | conf.Spin.Message(" local changes pushed") 154 | 155 | fmt.Fprintln(conf.OutWriter, "\nYour code was successfully added!") 156 | fmt.Fprintf(conf.OutWriter, "\n%s", script) 157 | return nil 158 | } 159 | 160 | func createTempFile(conf config.Config, storeBytes []byte) (string, error) { 161 | tempFile, err := config.GetTempStoreFilePath(conf) 162 | if err != nil { 163 | return tempFile, fmt.Errorf("failed to get the store temporary file path: %s", err) 164 | } 165 | 166 | //Copy the store file to a temporary destination 167 | if err := ioutil.WriteFile(tempFile, storeBytes, 0666); err != nil { 168 | return tempFile, fmt.Errorf("failed to write to file %s: %s", tempFile, err) 169 | } 170 | 171 | return tempFile, nil 172 | } 173 | 174 | func createHistoryTempFile(conf config.Config, storeBytes []byte) (string, error) { 175 | tempFile, err := config.GetTempHistoryStoreFilePath(conf) 176 | if err != nil { 177 | return tempFile, fmt.Errorf("failed to get the history store temporary file path: %s", err) 178 | } 179 | 180 | //Copy the store file to a temporary destination 181 | if err := ioutil.WriteFile(tempFile, storeBytes, 0666); err != nil { 182 | return tempFile, fmt.Errorf("failed to write to file %s: %s", tempFile, err) 183 | } 184 | 185 | return tempFile, nil 186 | } 187 | 188 | func loadStore(storeFile string) (string, *store.Store, []byte, error) { 189 | storeData, storeBytes, err := store.LoadStore(storeFile) 190 | if err != nil { 191 | return storeFile, storeData, storeBytes, fmt.Errorf("failed to load store: %s", err) 192 | } 193 | return storeFile, storeData, storeBytes, nil 194 | } 195 | 196 | func saveStore(storeData *store.Store, storeBytes []byte, storeFile, tempFile string) error { 197 | //Save the new `Store` struct in the store file 198 | if err := storeData.SaveStore(storeFile); err != nil { 199 | //if we failed to write the store file then we restore the file to its original content 200 | if err1 := ioutil.WriteFile(storeFile, storeBytes, 0666); err1 != nil { 201 | return fmt.Errorf("failed to write to file %s: %s", storeFile, err) 202 | } 203 | 204 | //Delete the temporary file 205 | if err := os.RemoveAll(tempFile); err != nil { 206 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 207 | } 208 | 209 | return fmt.Errorf("failed to write to file %s: %s", storeFile, err) 210 | } 211 | 212 | return nil 213 | } 214 | 215 | //createNewCodeScriptEntry return a new code Script entry 216 | func createNewCodeScriptEntry(code string, flags *flag.FlagSet) (store.Script, error) { 217 | timeNow := time.Now() 218 | 219 | alias, err := flags.GetString("alias") 220 | if err != nil { 221 | return store.Script{}, fmt.Errorf("could not parse `alias` flag: %s", err) 222 | } 223 | comment, err := flags.GetString("comment") 224 | if err != nil { 225 | return store.Script{}, fmt.Errorf("could not parse `comment` flag: %s", err) 226 | } 227 | path, err := flags.GetString("path") 228 | if err != nil { 229 | return store.Script{}, fmt.Errorf("could not parse `path` flag: %s", err) 230 | } 231 | 232 | if path != "" { 233 | bytes, err := ioutil.ReadFile(path) 234 | if err != nil { 235 | return store.Script{}, fmt.Errorf("failed to read %s: %s", path, err) 236 | } 237 | code = string(bytes) 238 | } 239 | 240 | //Generate script entry unique id 241 | id, err := store.GenereateIdempotentID(code, comment, alias, "") 242 | if err != nil { 243 | return store.Script{}, fmt.Errorf("failed to generate idem potent id: %s", err) 244 | } 245 | 246 | return store.Script{ 247 | ID: id, 248 | Comment: comment, 249 | CreationTime: timeNow, 250 | UpdateTime: timeNow, 251 | Code: store.Code{ 252 | Content: code, 253 | Alias: alias, 254 | }, 255 | }, nil 256 | } 257 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2021-05-31T15:58:39Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 113 | } 114 | echoerr() { 115 | echo "$@" 1>&2 116 | } 117 | log_prefix() { 118 | echo "$0" 119 | } 120 | _logp=6 121 | log_set_priority() { 122 | _logp="$1" 123 | } 124 | log_priority() { 125 | if test -z "$1"; then 126 | echo "$_logp" 127 | return 128 | fi 129 | [ "$1" -le "$_logp" ] 130 | } 131 | log_tag() { 132 | case $1 in 133 | 0) echo "emerg" ;; 134 | 1) echo "alert" ;; 135 | 2) echo "crit" ;; 136 | 3) echo "err" ;; 137 | 4) echo "warning" ;; 138 | 5) echo "notice" ;; 139 | 6) echo "info" ;; 140 | 7) echo "debug" ;; 141 | *) echo "$1" ;; 142 | esac 143 | } 144 | log_debug() { 145 | log_priority 7 || return 0 146 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 147 | } 148 | log_info() { 149 | log_priority 6 || return 0 150 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 151 | } 152 | log_err() { 153 | log_priority 3 || return 0 154 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 155 | } 156 | log_crit() { 157 | log_priority 2 || return 0 158 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 159 | } 160 | uname_os() { 161 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 162 | case "$os" in 163 | cygwin_nt*) os="windows" ;; 164 | mingw*) os="windows" ;; 165 | msys_nt*) os="windows" ;; 166 | esac 167 | echo "$os" 168 | } 169 | uname_arch() { 170 | arch=$(uname -m) 171 | case $arch in 172 | x86_64) arch="amd64" ;; 173 | x86) arch="386" ;; 174 | i686) arch="386" ;; 175 | i386) arch="386" ;; 176 | aarch64) arch="arm64" ;; 177 | armv5*) arch="armv5" ;; 178 | armv6*) arch="armv6" ;; 179 | armv7*) arch="armv7" ;; 180 | esac 181 | echo ${arch} 182 | } 183 | uname_os_check() { 184 | os=$(uname_os) 185 | case "$os" in 186 | darwin) return 0 ;; 187 | dragonfly) return 0 ;; 188 | freebsd) return 0 ;; 189 | linux) return 0 ;; 190 | android) return 0 ;; 191 | nacl) return 0 ;; 192 | netbsd) return 0 ;; 193 | openbsd) return 0 ;; 194 | plan9) return 0 ;; 195 | solaris) return 0 ;; 196 | windows) return 0 ;; 197 | esac 198 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 199 | return 1 200 | } 201 | uname_arch_check() { 202 | arch=$(uname_arch) 203 | case "$arch" in 204 | 386) return 0 ;; 205 | amd64) return 0 ;; 206 | arm64) return 0 ;; 207 | armv5) return 0 ;; 208 | armv6) return 0 ;; 209 | armv7) return 0 ;; 210 | ppc64) return 0 ;; 211 | ppc64le) return 0 ;; 212 | mips) return 0 ;; 213 | mipsle) return 0 ;; 214 | mips64) return 0 ;; 215 | mips64le) return 0 ;; 216 | s390x) return 0 ;; 217 | amd64p32) return 0 ;; 218 | esac 219 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 220 | return 1 221 | } 222 | untar() { 223 | tarball=$1 224 | case "${tarball}" in 225 | *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; 226 | *.tar) tar --no-same-owner -xf "${tarball}" ;; 227 | *.zip) unzip "${tarball}" ;; 228 | *) 229 | log_err "untar unknown archive format for ${tarball}" 230 | return 1 231 | ;; 232 | esac 233 | } 234 | http_download_curl() { 235 | local_file=$1 236 | source_url=$2 237 | header=$3 238 | if [ -z "$header" ]; then 239 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 240 | else 241 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 242 | fi 243 | if [ "$code" != "200" ]; then 244 | log_debug "http_download_curl received HTTP status $code" 245 | return 1 246 | fi 247 | return 0 248 | } 249 | http_download_wget() { 250 | local_file=$1 251 | source_url=$2 252 | header=$3 253 | if [ -z "$header" ]; then 254 | wget -q -O "$local_file" "$source_url" 255 | else 256 | wget -q --header "$header" -O "$local_file" "$source_url" 257 | fi 258 | } 259 | http_download() { 260 | log_debug "http_download $2" 261 | if is_command curl; then 262 | http_download_curl "$@" 263 | return 264 | elif is_command wget; then 265 | http_download_wget "$@" 266 | return 267 | fi 268 | log_crit "http_download unable to find wget or curl" 269 | return 1 270 | } 271 | http_copy() { 272 | tmp=$(mktemp) 273 | http_download "${tmp}" "$1" "$2" || return 1 274 | body=$(cat "$tmp") 275 | rm -f "${tmp}" 276 | echo "$body" 277 | } 278 | github_release() { 279 | owner_repo=$1 280 | version=$2 281 | test -z "$version" && version="latest" 282 | giturl="https://github.com/${owner_repo}/releases/${version}" 283 | json=$(http_copy "$giturl" "Accept:application/json") 284 | test -z "$json" && return 1 285 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 286 | test -z "$version" && return 1 287 | echo "$version" 288 | } 289 | hash_sha256() { 290 | TARGET=${1:-/dev/stdin} 291 | if is_command gsha256sum; then 292 | hash=$(gsha256sum "$TARGET") || return 1 293 | echo "$hash" | cut -d ' ' -f 1 294 | elif is_command sha256sum; then 295 | hash=$(sha256sum "$TARGET") || return 1 296 | echo "$hash" | cut -d ' ' -f 1 297 | elif is_command shasum; then 298 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 299 | echo "$hash" | cut -d ' ' -f 1 300 | elif is_command openssl; then 301 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 302 | echo "$hash" | cut -d ' ' -f a 303 | else 304 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 305 | return 1 306 | fi 307 | } 308 | hash_sha256_verify() { 309 | TARGET=$1 310 | checksums=$2 311 | if [ -z "$checksums" ]; then 312 | log_err "hash_sha256_verify checksum file not specified in arg2" 313 | return 1 314 | fi 315 | BASENAME=${TARGET##*/} 316 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 317 | if [ -z "$want" ]; then 318 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 319 | return 1 320 | fi 321 | got=$(hash_sha256 "$TARGET") 322 | if [ "$want" != "$got" ]; then 323 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 324 | return 1 325 | fi 326 | } 327 | cat /dev/null < 68 | Will edit the entry corresponding the entry_id 69 | `, 70 | Run: func(cmd *cobra.Command, args []string) { 71 | if err := editCommand(cmd, args, conf); err != nil { 72 | fmt.Fprintf(conf.OutWriter, "Error: %s\n", err) 73 | return 74 | } 75 | }, 76 | } 77 | 78 | command.PersistentFlags().Bool("from-history", false, `list code and solution records from history`) 79 | command.PersistentFlags().StringP("comment", "m", "", `ckp edit -m `) 80 | command.PersistentFlags().StringP("alias", "a", "", `ckp edit -a `) 81 | command.PersistentFlags().StringP("code", "c", "", `ckp edit -c `) 82 | command.PersistentFlags().StringP("solution", "s", "", `ckp edit -s `) 83 | command.PersistentFlags().BoolP("interactive", "i", false, `open a text editor`) 84 | return command 85 | } 86 | 87 | func editCommand(cmd *cobra.Command, args []string, conf config.Config) error { 88 | var entryID string 89 | if len(args) >= 1 { 90 | entryID = args[0] 91 | } 92 | 93 | if err := cmd.Flags().Parse(args); err != nil { 94 | return err 95 | } 96 | flags := cmd.Flags() 97 | fromHistory, err := flags.GetBool("from-history") 98 | if err != nil { 99 | return fmt.Errorf("could not parse `fromHistory` flag: %s", err) 100 | } 101 | 102 | isInteractive, err := flags.GetBool("interactive") 103 | if err != nil { 104 | return fmt.Errorf("could not parse `interactive` flag: %s", err) 105 | } 106 | 107 | //Setup spinner 108 | conf.Spin.Start() 109 | defer conf.Spin.Stop() 110 | 111 | dir, err := config.GetStoreDirPath(conf) 112 | if err != nil { 113 | return fmt.Errorf("failed get repository path: %s", err) 114 | } 115 | 116 | //Get the store file path 117 | var storeFilePath string 118 | if !fromHistory { 119 | storeFilePath, err = config.GetStoreFilePath(conf) 120 | if err != nil { 121 | return fmt.Errorf("failed to get the store file path: %s", err) 122 | } 123 | } else { 124 | storeFilePath, err = config.GetHistoryFilePath(conf) 125 | if err != nil { 126 | return fmt.Errorf("failed to get the history store file path: %s", err) 127 | } 128 | } 129 | 130 | conf.Spin.Message(" pulling remote changes...") 131 | err = pullRemoteChanges(conf, dir, storeFilePath) 132 | if err != nil { 133 | return fmt.Errorf("failed to pull remote changes: %s", err) 134 | } 135 | conf.Spin.Message(" remote changes pulled") 136 | 137 | conf.Spin.Message(" removing changes") 138 | storeFile, storeData, storeBytes, err := loadStore(storeFilePath) 139 | if err != nil { 140 | return fmt.Errorf("failed to load the store: %s", err) 141 | } 142 | 143 | index, err := getScriptEntryIndex(conf, storeData.Scripts, entryID, store.EntryTypeAll) 144 | if err != nil { 145 | return fmt.Errorf("failed to get script `%s` entry index: %s", entryID, err) 146 | } 147 | 148 | script, err := createNewEntry(flags, storeData.Scripts[index]) 149 | if err != nil { 150 | return fmt.Errorf("failed to create new script entry: %s", err) 151 | } 152 | 153 | //if it is an interactive update 154 | if len(args) == 0 || isInteractive { 155 | conf.Spin.Stop() 156 | s, err := getNewEntryDataFromFile(conf, script, EntryTemplateType) 157 | if err != nil { 158 | return fmt.Errorf("failed to get new entry from the editor %s", err) 159 | } 160 | 161 | //Generate an ID for the newly added script 162 | s.ID, err = store.GenereateIdempotentID(s.Code.Content, s.Comment, s.Code.Alias, s.Solution.Content) 163 | if err != nil { 164 | return fmt.Errorf("failed to generate idem potent ID: %s", err) 165 | } 166 | 167 | script.ID = s.ID 168 | script.Code = s.Code 169 | script.Solution = s.Solution 170 | script.Comment = s.Comment 171 | conf.Spin.Start() 172 | } 173 | 174 | //Check if entry already exist 175 | if storeData.EntryAlreadyExist(script.ID) { 176 | return fmt.Errorf("An identical record was found in the storage, please try `ckp edit %s`", script.ID) 177 | } 178 | 179 | //Remove script entry 180 | storeData.Scripts, err = editScriptEntry(storeData.Scripts, script, index) 181 | if err != nil { 182 | return fmt.Errorf("failed to edit script entry: %s", err) 183 | } 184 | 185 | tempFile, err := createTempFile(conf, storeBytes) 186 | if err != nil { 187 | return fmt.Errorf("failed to create tempFile: %s", err) 188 | } 189 | 190 | //Save storeData in store 191 | if err := saveStore(storeData, storeBytes, storeFile, tempFile); err != nil { 192 | return fmt.Errorf("failed to save store in %s: %s", storeFile, err) 193 | } 194 | 195 | //Delete the temporary file 196 | if err := os.RemoveAll(tempFile); err != nil { 197 | return fmt.Errorf("failed to delete file %s: %s", tempFile, err) 198 | } 199 | 200 | conf.Spin.Message(" pushing local changes...") 201 | err = pushLocalChanges(conf, dir, commitRemoveAction, storeFilePath) 202 | if err != nil { 203 | return fmt.Errorf("failed to push local changes: %s", err) 204 | } 205 | conf.Spin.Message(" local changes pushed") 206 | 207 | fmt.Fprintf(conf.OutWriter, "\nYour entry was successfully edited!\n") 208 | fmt.Fprintf(conf.OutWriter, "\n%s", storeData.Scripts[len(storeData.Scripts)-1]) 209 | return nil 210 | } 211 | 212 | func editScriptEntry(scripts []store.Script, script store.Script, index int) ([]store.Script, error) { 213 | return append(scripts[:index], append(scripts[index+1:], script)...), nil 214 | } 215 | 216 | //createNewEntry return a new code Script entry 217 | func createNewEntry(flags *pflag.FlagSet, script store.Script) (store.Script, error) { 218 | timeNow := time.Now() 219 | 220 | //Get alias 221 | alias := script.Code.Alias 222 | aliasTmp, err := flags.GetString("alias") 223 | if err != nil { 224 | return store.Script{}, fmt.Errorf("could not parse `alias` flag: %s", err) 225 | } 226 | if aliasTmp != "" { 227 | alias = aliasTmp 228 | } 229 | 230 | //Get comment 231 | comment := script.Comment 232 | commentTmp, err := flags.GetString("comment") 233 | if err != nil { 234 | return store.Script{}, fmt.Errorf("could not parse `comment` flag: %s", err) 235 | } 236 | if commentTmp != "" { 237 | comment = commentTmp 238 | } 239 | 240 | //Get code 241 | var code string 242 | if script.Code.Content != "" { 243 | code = script.Code.Content 244 | } 245 | codeTmp, err := flags.GetString("code") 246 | if err != nil { 247 | return store.Script{}, fmt.Errorf("could not parse `code` flag: %s", err) 248 | } 249 | if codeTmp != "" { 250 | code = codeTmp 251 | } 252 | 253 | //Get Solution 254 | var solution string 255 | if script.Solution.Content != "" { 256 | solution = script.Solution.Content 257 | } 258 | solutionTmp, err := flags.GetString("solution") 259 | if err != nil { 260 | return store.Script{}, fmt.Errorf("could not parse `solution` flag: %s", err) 261 | } 262 | if solutionTmp != "" { 263 | solution = solutionTmp 264 | } 265 | 266 | //Generate script entry unique id 267 | id, err := store.GenereateIdempotentID(code, comment, alias, solution) 268 | if err != nil { 269 | return store.Script{}, fmt.Errorf("failed to generate idem potent id: %s", err) 270 | } 271 | 272 | return store.Script{ 273 | ID: id, 274 | Comment: comment, 275 | CreationTime: timeNow, 276 | UpdateTime: timeNow, 277 | Code: store.Code{ 278 | Content: code, 279 | Alias: alias, 280 | }, 281 | Solution: store.Solution{ 282 | Content: solution, 283 | }, 284 | }, nil 285 | } 286 | 287 | func getNewEntryDataFromFile(conf config.Config, origEntry store.Script, templateType string) (store.Script, error) { 288 | s := origEntry 289 | content := "" 290 | 291 | //Get template content 292 | switch templateType { 293 | case CodeEntryTemplateType: 294 | content = fmt.Sprintf(editorFileCodeTemplate, origEntry.Comment, origEntry.Code.Alias, origEntry.Code.Content) 295 | case SolutionEntryTemplateType: 296 | content = fmt.Sprintf(editorFileSolutionTemplate, origEntry.Comment, origEntry.Solution.Content) 297 | case EntryTemplateType: 298 | content = fmt.Sprintf(editorFileTemplate, origEntry.ID, origEntry.Comment, origEntry.Code.Alias, origEntry.Code.Content, origEntry.Solution.Content) 299 | } 300 | 301 | dir, err := config.GetDirPath(conf) 302 | if err != nil { 303 | return s, err 304 | } 305 | destination := fmt.Sprintf("%s/entry.%s.sh", dir, origEntry.ID) 306 | 307 | //Create the file with the original script data 308 | if err = ioutil.WriteFile(destination, []byte(content), 0666); err != nil { 309 | return s, fmt.Errorf("failed to write to file %s: %s", destination, err) 310 | } 311 | 312 | editor := conf.Viper.GetString("editor") 313 | //Open and edit that file 314 | err = conf.Exec.OpenEditor(editor, destination) 315 | if err != nil { 316 | return s, err 317 | } 318 | 319 | s, err = parseDataFromEditorTemplateFile(destination, templateType) 320 | if err != nil { 321 | return s, fmt.Errorf("failed to parse data from template file file %s: %s", destination, err) 322 | } 323 | 324 | //Delete the temporary file 325 | if err := os.RemoveAll(destination); err != nil { 326 | return s, fmt.Errorf("failed to delete file %s: %s", destination, err) 327 | } 328 | 329 | return s, nil 330 | } 331 | 332 | func parseDataFromEditorTemplateFile(filepath string, templateType string) (store.Script, error) { 333 | //get store from template file 334 | if _, err := os.Stat(filepath); os.IsNotExist(err) { 335 | return store.Script{}, err 336 | } 337 | 338 | data, err := ioutil.ReadFile(filepath) 339 | if err != nil { 340 | return store.Script{}, fmt.Errorf("failed to read file: %s", err) 341 | } 342 | 343 | switch templateType { 344 | case CodeEntryTemplateType: 345 | return parseCodeDataFromEditorTemplateString(string(data)), nil 346 | case SolutionEntryTemplateType: 347 | return parseSolutionDataFromEditorTemplateString(string(data)), nil 348 | } 349 | 350 | return parseDataFromEditorTemplateString(string(data)), nil 351 | } 352 | 353 | func parseDataFromEditorTemplateString(data string) store.Script { 354 | lines := strings.Split(data, "\n") 355 | 356 | //get comment 357 | i := moveToNextEntry(lines, 0) 358 | comment, i := getEntry(lines, i) 359 | 360 | //get alias 361 | i = moveToNextEntry(lines, i) 362 | alias, i := getEntry(lines, i) 363 | 364 | //get code 365 | i = moveToNextEntry(lines, i) 366 | code, i := getEntry(lines, i) 367 | 368 | //get solution 369 | i = moveToNextEntry(lines, i) 370 | solution, _ := getEntry(lines, i) 371 | 372 | if code != "" { 373 | return store.Script{ 374 | Comment: comment, 375 | Code: store.Code{ 376 | Content: code, 377 | Alias: alias, 378 | }, 379 | Solution: store.Solution{}, 380 | } 381 | } 382 | 383 | return store.Script{ 384 | Comment: comment, 385 | Solution: store.Solution{ 386 | Content: solution, 387 | }, 388 | Code: store.Code{}, 389 | } 390 | } 391 | 392 | func parseCodeDataFromEditorTemplateString(data string) store.Script { 393 | lines := strings.Split(data, "\n") 394 | 395 | //get comment 396 | comment, _ := getComment(lines) 397 | 398 | //get alias 399 | alias, _ := getAlias(lines) 400 | 401 | //get code 402 | code := getCode(lines) 403 | 404 | return store.Script{ 405 | Comment: comment, 406 | Code: store.Code{ 407 | Content: code, 408 | Alias: alias, 409 | }, 410 | Solution: store.Solution{}, 411 | } 412 | } 413 | 414 | func parseSolutionDataFromEditorTemplateString(data string) store.Script { 415 | lines := strings.Split(data, "\n") 416 | 417 | //get comment 418 | comment, _ := getComment(lines) 419 | 420 | //get solution 421 | solution := getSolution(lines) 422 | 423 | return store.Script{ 424 | Comment: comment, 425 | Solution: store.Solution{ 426 | Content: solution, 427 | }, 428 | Code: store.Code{}, 429 | } 430 | } 431 | 432 | //moveToNextEntry skips comments and return the index to the next valid line 433 | func moveToNextEntry(lines []string, i int) int { 434 | if i >= len(lines) { 435 | return i - 1 436 | } 437 | 438 | for i := i; i < len(lines); i++ { 439 | line := lines[i] 440 | //if "##" is not at the beginning of the line 441 | if strings.Index(line, "##") != 0 { 442 | return i 443 | } 444 | } 445 | 446 | return i 447 | } 448 | 449 | //getComment iterate over the file content and returns the first `## comment:` line 450 | // it returns the comment it found or an error if no comment was founds 451 | func getComment(lines []string) (string, error) { 452 | for _, line := range lines { 453 | if strings.Contains(line, "## comment") { 454 | s := strings.Split(line, ":") 455 | if len(s) >= 2 { 456 | return strings.Join(s[1:], ""), nil 457 | } 458 | } 459 | } 460 | 461 | return "", fmt.Errorf("comment not found") 462 | } 463 | 464 | //getAlias iterate over the file content and returns the first `## alias:` line 465 | // it returns the alias it found or an error if no alias was founds 466 | func getAlias(lines []string) (string, error) { 467 | for _, line := range lines { 468 | if strings.Contains(line, "## alias") { 469 | s := strings.Split(line, ":") 470 | if len(s) >= 2 { 471 | return strings.Join(s[1:], ""), nil 472 | } 473 | } 474 | } 475 | 476 | return "", fmt.Errorf("alias not found") 477 | } 478 | 479 | //getCode iterate over the file content and returns 480 | //everything that does not start with "##" as a code 481 | func getCode(lines []string) string { 482 | code := "" 483 | for _, line := range lines { 484 | //if "##" is at the beginning of the line 485 | if strings.Index(line, "##") != 0 { 486 | code += line + "\n" 487 | } 488 | } 489 | 490 | return strings.Trim(code, "\n") 491 | } 492 | 493 | //getSolution iterate over the file content and returns 494 | //everything that does not start with "##" as a solution 495 | func getSolution(lines []string) string { 496 | solution := "" 497 | for _, line := range lines { 498 | //if "##" is at the beginning of the line 499 | if strings.Index(line, "##") != 0 { 500 | solution += line + "\n" 501 | } 502 | } 503 | 504 | return strings.Trim(solution, "\n") 505 | } 506 | 507 | //getEntry returns the entry and returns an index to the next line 508 | func getEntry(lines []string, i int) (string, int) { 509 | entry := "" 510 | if i >= len(lines) { 511 | return entry, i - 1 512 | } 513 | 514 | for i := i; i < len(lines); i++ { 515 | line := lines[i] 516 | //if "##" is at the beginning of the line 517 | if strings.Index(line, "##") == 0 { 518 | return strings.Trim(entry, "\n"), i 519 | } 520 | 521 | entry += line + "\n" 522 | } 523 | 524 | return strings.Trim(entry, "\n"), i 525 | } 526 | --------------------------------------------------------------------------------