changeset 5:af840bc25791

impl edit cmd
author anatofuz <anatofuz@cr.ie.u-ryukyu.ac.jp>
date Fri, 04 Dec 2020 18:27:09 +0900
parents 359eff175bf1
children 0b9932242273
files client.go client/client.go client/page.go cmd_edit.go cmd_new.go cmd_push.go config.go editor.go go.mod go.sum growsync.go util.go
diffstat 12 files changed, 404 insertions(+), 82 deletions(-) [+]
line wrap: on
line diff
--- a/client.go	Tue Dec 01 21:37:47 2020 +0900
+++ b/client.go	Fri Dec 04 18:27:09 2020 +0900
@@ -2,17 +2,18 @@
 
 import (
 	"context"
+	"fmt"
 	"io/ioutil"
 
-	"github.com/crowi/go-crowi"
+	"www.cr.ie.u-ryukyu.ac.jp/hg/Members/anatofuz/growsync/client"
 )
 
 type growClient struct {
-	client *crowi.Client
+	client *client.Client
 }
 
 func NewGrowiClient(url, token string) (*growClient, error) {
-	client, err := crowi.NewClient(crowi.Config{URL: url, Token: token})
+	client, err := client.NewClient(client.Config{URL: url, Token: token})
 	if err != nil {
 		return nil, err
 	}
@@ -22,13 +23,17 @@
 	return &gClient, nil
 }
 
-func (gClient *growClient) CheckIsExistsToken(path string) (bool, error) {
+func (gClient *growClient) IsExistsPageOnGrowi(path string) (*client.Page, error) {
 	ctx := context.Background()
 	page, err := gClient.client.Pages.Get(ctx, path)
+
+	if err == client.ErrorPageNotFOund {
+		return nil, nil
+	}
 	if err != nil {
-		return false, err
+		return nil, err
 	}
-	return page.OK, nil
+	return page, nil
 }
 
 func (gClient *growClient) CreateNewPage(path string, mdPATH string) error {
@@ -47,6 +52,18 @@
 	if err != nil {
 		return err
 	}
-	_, err = gClient.client.Pages.Update(ctx, path, string(markdown))
+
+	page, err := gClient.IsExistsPageOnGrowi(path)
+
+	if err != nil {
+		return err
+	}
+
+	if page == nil {
+		fmt.Println("[info] create new page", mdPATH)
+		return gClient.CreateNewPage(path, mdPATH)
+	}
+
+	_, err = gClient.client.Pages.Update(ctx, page.ID, page.Revision.ID, string(markdown))
 	return err
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/client.go	Fri Dec 04 18:27:09 2020 +0900
@@ -0,0 +1,97 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"path"
+	"strings"
+)
+
+type ID string
+
+type Config struct {
+	URL   string
+	Token string
+}
+
+type Client struct {
+	httpClient *http.Client
+	config     *Config
+
+	Pages *PagesService
+}
+
+type service struct {
+	client *Client
+}
+
+func NewClient(cfg Config) (*Client, error) {
+	if cfg.URL == "" {
+		return nil, fmt.Errorf("missing growi url")
+	}
+
+	if cfg.Token == "" {
+		return nil, fmt.Errorf("missing api token")
+	}
+
+	httpClient := http.DefaultClient
+
+	client := Client{
+		httpClient: httpClient,
+		config:     &cfg,
+	}
+	client.Pages = &PagesService{client: &client}
+	return &client, nil
+}
+
+func isMethod(method string) bool {
+	for _, httpMethod := range []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPut} {
+		if method == httpMethod {
+			return true
+		}
+	}
+	return false
+}
+
+func (c *Client) newRequest(ctx context.Context, method string, uri string, params *url.Values) ([]byte, error) {
+	if !isMethod(method) {
+		return nil, fmt.Errorf("failed not http metod %s", method)
+	}
+
+	u, err := url.Parse(c.config.URL)
+	if err != nil {
+		return nil, err
+	}
+
+	var body io.Reader
+	if method == http.MethodGet {
+		u.RawQuery = params.Encode()
+	} else {
+		body = strings.NewReader(params.Encode())
+	}
+
+	u.Path = path.Join(u.Path, uri)
+
+	fmt.Printf("[info] method %s, url %s body %s\n", method, u.String(), body)
+	req, err := http.NewRequest(method, u.String(), body)
+	req = req.WithContext(ctx)
+
+	if params != nil {
+		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	}
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	content, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	return content, nil
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/page.go	Fri Dec 04 18:27:09 2020 +0900
@@ -0,0 +1,152 @@
+package client
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+type PagesService service
+
+type User struct {
+	IsGravatarEnabled bool      `json:"isGravatarEnabled"`
+	IsEmailPublished  bool      `json:"isEmailPublished"`
+	Lang              string    `json:"lang"`
+	Status            int       `json:"status"`
+	Admin             bool      `json:"admin"`
+	ID                string    `json:"_id"`
+	CreatedAt         time.Time `json:"createdAt"`
+	Name              string    `json:"name"`
+	Username          string    `json:"username"`
+	Email             string    `json:"email"`
+	LastLoginAt       time.Time `json:"lastLoginAt"`
+	ImageURLCached    string    `json:"imageUrlCached"`
+}
+
+type Revision struct {
+	Format        string    `json:"format"`
+	ID            string    `json:"_id"`
+	CreatedAt     time.Time `json:"createdAt"`
+	Path          string    `json:"path"`
+	Body          string    `json:"body"`
+	Author        User      `json:"author"`
+	HasDiffToPrev bool      `json:"hasDiffToPrev"`
+	V             int       `json:"__v"`
+}
+
+type Page struct {
+	Status         string        `json:"status"`
+	Grant          int           `json:"grant"`
+	GrantedUsers   []interface{} `json:"grantedUsers"`
+	Liker          []interface{} `json:"liker"`
+	SeenUsers      []string      `json:"seenUsers"`
+	CommentCount   int           `json:"commentCount"`
+	Extended       string        `json:"extended"`
+	SubID          string        `json:"_id"`
+	CreatedAt      time.Time     `json:"createdAt"`
+	UpdatedAt      time.Time     `json:"updatedAt"`
+	Path           string        `json:"path"`
+	Creator        User          `json:"creator"`
+	LastUpdateUser User          `json:"lastUpdateUser"`
+	RedirectTo     interface{}   `json:"redirectTo"`
+	GrantedGroup   interface{}   `json:"grantedGroup"`
+	V              int           `json:"__v"`
+	ID             string        `json:"id"`
+	Revision       Revision      `json:"revision"`
+}
+
+type PagesGet struct {
+	Page  Page   `json:"page"`
+	Ok    bool   `json:"ok"`
+	Error string `json:"error"`
+}
+
+type PagesCreate struct {
+	Page     Page     `json:"page"`
+	Ok       bool     `json:"ok"`
+	Revision Revision `json:"revision"`
+	Error    string   `json:"error"`
+}
+
+type UpdateParams struct {
+	PageID     string
+	RevisionID string
+}
+
+var ErrorPageNotFOund = fmt.Errorf("page not found")
+
+const CREATE_ENDPOINT string = "/_api/pages.create"
+
+// Create makes a page in your Crowi. The request requires
+// the path and page content used for the page name
+func (p *PagesService) Create(ctx context.Context, path, body string) (*PagesCreate, error) {
+	fmt.Printf("path %s, body %s\n", path, body)
+	params := url.Values{}
+	params.Add("access_token", p.client.config.Token)
+	params.Add("path", path)
+	params.Add("body", body)
+	res, err := p.client.newRequest(ctx, http.MethodPost, CREATE_ENDPOINT, &params)
+	if err != nil {
+		fmt.Println("[error] failed send client request")
+		fmt.Println(string(res))
+		return nil, err
+	}
+
+	createPages := PagesCreate{}
+	if err := json.Unmarshal(res, &createPages); err != nil {
+		fmt.Println("[error] failed unmarshal json resupons")
+		fmt.Println(string(res))
+		return nil, err
+	}
+
+	return &createPages, nil
+}
+
+const GET_ENDPOINT string = "/_api/pages.get"
+
+func (p *PagesService) Get(ctx context.Context, path string) (*Page, error) {
+	params := url.Values{}
+	params.Add("access_token", p.client.config.Token)
+	params.Add("path", path)
+
+	res, err := p.client.newRequest(ctx, http.MethodGet, GET_ENDPOINT, &params)
+	if err != nil {
+		return nil, err
+	}
+	pagesGet := PagesGet{}
+	if err := json.Unmarshal(res, &pagesGet); err != nil {
+		return nil, err
+	}
+
+	if !pagesGet.Ok {
+		return nil, ErrorPageNotFOund
+	}
+
+	return &pagesGet.Page, nil
+}
+
+const UPDATE_ENDPOINT string = "/_api/pages.update"
+
+func (p *PagesService) Update(ctx context.Context, pageID, revisionID, body string) (*Page, error) {
+	params := url.Values{}
+	params.Add("access_token", p.client.config.Token)
+	params.Add("page_id", pageID)
+	params.Add("revision_id", revisionID)
+	params.Add("body", body)
+
+	res, err := p.client.newRequest(ctx, http.MethodPost, UPDATE_ENDPOINT, &params)
+	if err != nil {
+		return nil, err
+	}
+	page := Page{}
+	if err := json.Unmarshal(res, &page); err != nil {
+		fmt.Println("[error] failed unmarshal json resupons")
+		fmt.Println(string(res))
+		return nil, err
+	}
+
+	return &page, nil
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd_edit.go	Fri Dec 04 18:27:09 2020 +0900
@@ -0,0 +1,58 @@
+package growsync
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"path/filepath"
+	"time"
+)
+
+type editCmd struct{}
+
+const layout string = "2006/01/02"
+
+func (pc *editCmd) name() string {
+	return "edit"
+}
+
+func (pc *editCmd) description() string {
+	return "edit from growi web app"
+}
+
+func (pc *editCmd) run(ctx context.Context, argv []string, config *growiConfig, stdWriter io.Writer, errorWriter io.Writer) error {
+	fs := flag.NewFlagSet("growsync edit", flag.ContinueOnError)
+	fs.SetOutput(errorWriter)
+
+	if err := fs.Parse(argv); err != nil {
+		return nil
+	}
+
+	var growiPATH string
+
+	if fs.NArg() < 1 {
+		growiPATH = createNewDailyMarkdownPATH(config.DailyPATH)
+	} else {
+		growiPATH = convertGrowiSystemPath(fs.Arg(0))
+	}
+
+	localFilePATH := filepath.Join(config.LocalRoot, growiPATH+".md")
+
+	client, err := NewGrowiClient(config.URL, config.TOKEN)
+	if err != nil {
+		return err
+	}
+
+	err = doEdit(localFilePATH)
+	if err != nil {
+		return fmt.Errorf("failed edit mardkwodn file %+v", err)
+	}
+
+	return client.UpdatePage(growiPATH, localFilePATH)
+}
+
+func createNewDailyMarkdownPATH(dailyPATH string) string {
+	now := time.Now()
+	return filepath.Join(dailyPATH, now.Format(layout))
+}
--- a/cmd_new.go	Tue Dec 01 21:37:47 2020 +0900
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-package growsync
-
-import (
-	"context"
-	"flag"
-	"fmt"
-	"io"
-	"path/filepath"
-	"time"
-)
-
-type newCmd struct{}
-
-const layout string = "2006/01/02"
-
-func (pc *newCmd) name() string {
-	return "new"
-}
-
-func (pc *newCmd) description() string {
-	return "new from growi web app"
-}
-
-func (pc *newCmd) run(ctx context.Context, argv []string, config *growiConfig, stdWriter io.Writer, errorWriter io.Writer) error {
-	fs := flag.NewFlagSet("growsync new", flag.ContinueOnError)
-	fs.SetOutput(errorWriter)
-
-	if err := fs.Parse(argv); err != nil {
-		return nil
-	}
-
-	var newMarkdownPATH string
-
-	if fs.NArg() < 1 {
-		newMarkdownPATH = createNewDailyMarkdownPATH(config.DailyPATH)
-	} else {
-		newMarkdownPATH = fs.Arg(0)
-	}
-
-	_, err := NewGrowiClient(config.URL, config.TOKEN)
-	if err != nil {
-		return err
-	}
-
-	fmt.Println(newMarkdownPATH)
-	return nil
-}
-
-func createNewDailyMarkdownPATH(dailyPATH string) string {
-	now := time.Now()
-	return filepath.Join(dailyPATH, now.Format(layout))
-}
--- a/cmd_push.go	Tue Dec 01 21:37:47 2020 +0900
+++ b/cmd_push.go	Fri Dec 04 18:27:09 2020 +0900
@@ -3,7 +3,6 @@
 import (
 	"context"
 	"flag"
-	"fmt"
 	"io"
 
 	"golang.org/x/xerrors"
@@ -38,12 +37,15 @@
 
 	markdownPATH := fs.Arg(0)
 	growiSystemPath := convertGrowiSystemPath(markdownPATH)
-	isExists, err := client.CheckIsExistsToken(growiSystemPath)
-	fmt.Println(isExists)
+	id, err := client.IsExistsPageOnGrowi(growiSystemPath)
 
-	if isExists {
-		return client.UpdatePage(growiSystemPath, markdownPATH)
+	if err != nil {
+		return err
 	}
 
-	return client.CreateNewPage(growiSystemPath, markdownPATH)
+	if id == nil {
+		return client.CreateNewPage(growiSystemPath, markdownPATH)
+	}
+
+	return client.UpdatePage(growiSystemPath, markdownPATH)
 }
--- a/config.go	Tue Dec 01 21:37:47 2020 +0900
+++ b/config.go	Fri Dec 04 18:27:09 2020 +0900
@@ -37,16 +37,8 @@
 		return "", err
 	}
 	configFilePATH := filepath.Join(home, ".config", "growsync", "config.yaml")
-	if !fileCheck(configFilePATH) {
+	if !existsFile(configFilePATH) {
 		return "", fmt.Errorf("[ERROR] conf file not found")
 	}
 	return configFilePATH, nil
 }
-
-func fileCheck(conf string) bool {
-	info, err := os.Stat(conf)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return !info.IsDir()
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/editor.go	Fri Dec 04 18:27:09 2020 +0900
@@ -0,0 +1,31 @@
+package growsync
+
+import (
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/mattn/go-tty"
+)
+
+func doEdit(mdfilePATH string) error {
+	editor := os.Getenv("EDITOR")
+	if editor == "" {
+		editor = "vi"
+	}
+	tty, err := tty.Open()
+	if err != nil {
+		return err
+	}
+	defer tty.Close()
+
+	editorWithArgs := strings.Fields(editor)
+	editorWithArgs = append(editorWithArgs, mdfilePATH)
+
+	cmd := exec.Command(editorWithArgs[0], editorWithArgs[1:]...)
+	cmd.Stdin = tty.Input()
+	cmd.Stdout = tty.Output()
+	cmd.Stderr = tty.Output()
+
+	return cmd.Run()
+}
--- a/go.mod	Tue Dec 01 21:37:47 2020 +0900
+++ b/go.mod	Fri Dec 04 18:27:09 2020 +0900
@@ -3,9 +3,9 @@
 go 1.15
 
 require (
-	github.com/crowi/go-crowi v0.0.0-20170809061107-ef04e39ae1ac
 	github.com/goccy/go-yaml v1.8.4
+	github.com/mattn/go-tty v0.0.3
 	github.com/pkg/errors v0.9.1
-	golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
+	golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
 )
--- a/go.sum	Tue Dec 01 21:37:47 2020 +0900
+++ b/go.sum	Fri Dec 04 18:27:09 2020 +0900
@@ -1,40 +1,56 @@
-github.com/crowi/go-crowi v0.0.0-20170809061107-ef04e39ae1ac h1:52GTmB0wFiTZTKBG/n1g84xum2dL5lkhN1gXtY3xRSo=
-github.com/crowi/go-crowi v0.0.0-20170809061107-ef04e39ae1ac/go.mod h1:fHmF2Li4ysI3D5EXN3eUO4FEEumXn+S6DUrl/F6Gxlg=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/goccy/go-yaml v1.8.4 h1:AOEdR7aQgbgwHznGe3BLkDQVujxCPUpHOZZcQcp8Y3M=
 github.com/goccy/go-yaml v1.8.4/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
 github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
+github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
--- a/growsync.go	Tue Dec 01 21:37:47 2020 +0900
+++ b/growsync.go	Fri Dec 04 18:27:09 2020 +0900
@@ -13,7 +13,7 @@
 const cmdName = "growsync"
 
 var (
-	subCommands       = []cmd{&newCmd{}, &pushCmd{}, &rootCmd{}, &versionCmd{}}
+	subCommands       = []cmd{&editCmd{}, &pushCmd{}, &rootCmd{}, &versionCmd{}}
 	dispatch          = make(map[string]cmd, len(subCommands))
 	maxSubcommandName int
 )
--- a/util.go	Tue Dec 01 21:37:47 2020 +0900
+++ b/util.go	Fri Dec 04 18:27:09 2020 +0900
@@ -1,6 +1,7 @@
 package growsync
 
 import (
+	"os"
 	"path/filepath"
 	"strings"
 )
@@ -23,3 +24,11 @@
 
 	return outputPATH
 }
+
+func existsFile(conf string) bool {
+	info, err := os.Stat(conf)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return !info.IsDir()
+}