From 1897ec522a84093ca5accc466aaf683d9af82ee3 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 21:41:16 +0200 Subject: [PATCH 01/28] incremented version [34m0s] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1d0ba9e..8f0916f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +0.5.0 From 56d4c20f9f6310d35aba8c349b9b300ec96e5616 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 22:02:55 +0200 Subject: [PATCH 02/28] provide a default configuration for commit message template [21m0s] --- command/status.go | 32 ++++++++++++++++++++++++++++++-- model/config.go | 9 +++++++++ model/model.go | 8 +++++++- 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 model/config.go diff --git a/command/status.go b/command/status.go index a6a7218..1a4dacb 100644 --- a/command/status.go +++ b/command/status.go @@ -5,6 +5,7 @@ import ( "os" "strconv" "strings" + "text/template" "time" "github.com/codegangsta/cli" @@ -57,6 +58,11 @@ func (c *Status) Run(ctx *cli.Context) error { return errwrap.Wrapf(fmt.Sprintf("Failed to get Daemon address: {{err}}"), err) } + conf, err := m.ReadConfig() + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to read configuration: {{err}}"), err) + } + client := NewClient(info) status, err := client.GetStatus() if err != nil { @@ -72,8 +78,8 @@ func (c *Status) Run(ctx *cli.Context) error { return errwrap.Wrapf(fmt.Sprintf("Failed to parse '%s' as a time duration: {{err}}", status.Time), err) } - //simple semver check if !ctx.Bool("time-only") { + //simple semver check curr, _ := strconv.Atoi(strings.Replace(status.CurrentVersion, ".", "", 2)) recent, _ := strconv.Atoi(strings.Replace(status.MostRecentVersion, ".", "", 2)) if curr != 0 && recent > curr { @@ -85,7 +91,29 @@ func (c *Status) Run(ctx *cli.Context) error { return nil } - fmt.Printf(" [%s]", t) + //parse temlate and only report error if we're talking to a human + tmpl, err := template.New("commit-msg").Parse(conf.CommitMessage) + if err != nil { + //@todo find a more elegant way to 'print' this for script usage + if ctx.Bool("time-only") { + return nil + } else { + return errwrap.Wrapf(fmt.Sprintf("Failed to parse commit_message: '%s' in configuration as a text/template: {{err}}", conf.CommitMessage), err) + } + } + + //execute template and write to stdout + err = tmpl.Execute(os.Stdout, t) + if err != nil { + //@todo find a more elegant way to 'print' this for script usage + if ctx.Bool("time-only") { + return nil + } else { + return errwrap.Wrapf(fmt.Sprintf("Failed to execute commit_message: template for time '%s': {{err}}", t), err) + } + } + + //end with newline if we're printing for a human if !ctx.Bool("time-only") { fmt.Println() } diff --git a/model/config.go b/model/config.go new file mode 100644 index 0000000..0982e8f --- /dev/null +++ b/model/config.go @@ -0,0 +1,9 @@ +package model + +var DefaultConfig = &Config{ + CommitMessage: " [{{.}}]", +} + +type Config struct { + CommitMessage string `json:"commit_message"` +} diff --git a/model/model.go b/model/model.go index 999a5de..c317609 100644 --- a/model/model.go +++ b/model/model.go @@ -49,8 +49,10 @@ func (m *Model) Open() (*bolt.DB, error) { } func (m *Model) Close(db *bolt.DB) { + err := db.Close() + //@todo handle error? - db.Close() + _ = err } func (m *Model) UpsertDaemonInfo(info *Daemon) error { @@ -103,3 +105,7 @@ func (m *Model) ReadDaemonInfo() (*Daemon, error) { return nil }) } + +func (m *Model) ReadConfig() (*Config, error) { + return DefaultConfig, nil +} From 57f24f01acbca129cc817a9e5c7c10ed2a15cf70 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 22:14:51 +0200 Subject: [PATCH 03/28] now reads optional configuration file in repo root [spent 12m0s] --- command/status.go | 5 +++++ model/model.go | 23 ++++++++++++++++++++++- timeglass.json | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 timeglass.json diff --git a/command/status.go b/command/status.go index 1a4dacb..56ebbe2 100644 --- a/command/status.go +++ b/command/status.go @@ -60,6 +60,11 @@ func (c *Status) Run(ctx *cli.Context) error { conf, err := m.ReadConfig() if err != nil { + //@todo find a more elegant way to 'print' this for script usage + if ctx.Bool("time-only") { + return nil + } + return errwrap.Wrapf(fmt.Sprintf("Failed to read configuration: {{err}}"), err) } diff --git a/model/model.go b/model/model.go index c317609..00adb8a 100644 --- a/model/model.go +++ b/model/model.go @@ -2,6 +2,7 @@ package model import ( "crypto/md5" + "encoding/json" "fmt" "os" "os/user" @@ -107,5 +108,25 @@ func (m *Model) ReadDaemonInfo() (*Daemon, error) { } func (m *Model) ReadConfig() (*Config, error) { - return DefaultConfig, nil + conf := DefaultConfig + + p := filepath.Join(m.repoDir, "timeglass.json") + f, err := os.Open(p) + if err != nil { + if err == os.ErrNotExist { + return conf, nil + } + + return nil, errwrap.Wrapf(fmt.Sprintf("Error opening configuration file '%s', it does exist but: {{err}}", p), err) + } + + dec := json.NewDecoder(f) + + defer f.Close() + err = dec.Decode(conf) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Error decoding '%s' as JSON, please check for syntax errors: {{err}}", p), err) + } + + return conf, nil } diff --git a/timeglass.json b/timeglass.json new file mode 100644 index 0000000..4a67b63 --- /dev/null +++ b/timeglass.json @@ -0,0 +1,3 @@ +{ + "commit_message": " [spent {{.}}]" +} \ No newline at end of file From 83632cf8dd029e2f5a756eaeaaa4f4d42031f292 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 22:34:16 +0200 Subject: [PATCH 04/28] configuration file now also supports mbu spec [spent 20m0s] --- docs/config.md | 0 model/config.go | 28 ++++++++++++++++++++++++++++ model/model.go | 4 ++++ timeglass.json | 1 + 4 files changed, 33 insertions(+) create mode 100644 docs/config.md diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..e69de29 diff --git a/model/config.go b/model/config.go index 0982e8f..18c70b9 100644 --- a/model/config.go +++ b/model/config.go @@ -1,9 +1,37 @@ package model +import ( + "strconv" + "time" + + "github.com/hashicorp/errwrap" +) + +type MBU time.Duration + +func (m MBU) String() string { return time.Duration(m).String() } + +func (t *MBU) UnmarshalJSON(data []byte) error { + raw, err := strconv.Unquote(string(data)) + if err != nil { + return err + } + + parsed, err := time.ParseDuration(raw) + if err != nil { + return errwrap.Wrapf("Failed to parse duration: {{err}}", err) + } + + *t = MBU(parsed) + return nil +} + var DefaultConfig = &Config{ + MBU: MBU(time.Minute), CommitMessage: " [{{.}}]", } type Config struct { + MBU MBU `json:"mbu"` CommitMessage string `json:"commit_message"` } diff --git a/model/model.go b/model/model.go index 00adb8a..59db710 100644 --- a/model/model.go +++ b/model/model.go @@ -128,5 +128,9 @@ func (m *Model) ReadConfig() (*Config, error) { return nil, errwrap.Wrapf(fmt.Sprintf("Error decoding '%s' as JSON, please check for syntax errors: {{err}}", p), err) } + if time.Duration(conf.MBU) < time.Minute { + return nil, fmt.Errorf("configuration 'mbu': An MBU of less then 1min is not supported, received: '%s'", conf.MBU) + } + return conf, nil } diff --git a/timeglass.json b/timeglass.json index 4a67b63..1be9e74 100644 --- a/timeglass.json +++ b/timeglass.json @@ -1,3 +1,4 @@ { + "mbu": "1m10s", "commit_message": " [spent {{.}}]" } \ No newline at end of file From 42ad3d6e34482e3599bda746a4ade91ec51aac63 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 22:46:18 +0200 Subject: [PATCH 05/28] mbu config is now actually used by the daemon [spent 3m30s] --- command/start.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/command/start.go b/command/start.go index 7cf774f..e92182e 100644 --- a/command/start.go +++ b/command/start.go @@ -51,6 +51,11 @@ func (c *Start) Run(ctx *cli.Context) error { return errwrap.Wrapf(fmt.Sprintf("Failed to get Daemon address: {{err}}"), err) } + conf, err := m.ReadConfig() + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to read configuration: {{err}}"), err) + } + client := NewClient(info) err = client.Call("timer.start") if err != nil { @@ -58,7 +63,7 @@ func (c *Start) Run(ctx *cli.Context) error { return err } - cmd := exec.Command("glass-daemon") + cmd := exec.Command("glass-daemon", fmt.Sprintf("--mbu=%s", conf.MBU)) err := cmd.Start() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to start Daemon: {{err}}"), err) From f23648b0b2e556e246f7568f2332303b0c12c2ac Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 23:10:19 +0200 Subject: [PATCH 06/28] added documentation for configuration options [spent 28m0s] --- README.md | 4 ++++ docs/config.md | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 94bb714..d2b8549 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ __Currently Supported: (see roadmap)__ git log -n 1 ``` +## Documentation +More documentation will be available in the future, for now we offer the following: + +- [Configuration Reference](/docs/config.md) ## Roadmap, input welcome! diff --git a/docs/config.md b/docs/config.md index e69de29..1a4c69c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -0,0 +1,24 @@ +# Configuration +Timeglass can be configured by creating a `timeglass.json` file in the root of the repository you are tracking. Timeglass only accepts valid JSON, so if something seems wrong make sure to check your formatting. + +## Example +The following example shows all options with their default configuration: + +```json +{ + "mbu": "1m", + "commit_message": " [{{.}}]" +} +``` + +## MBU +__key__: `mbu` + +A timer runs in the background and increments by set amount of time eacht tick: the "minimal billable unit". It accepts a human readable format that is parsed by: [time.ParseDuration()](http://golang.org/pkg/time/#ParseDuration) + +## Commit Message Template +__key__: `commit_message` + +You can specify how you would like write the time you spent to the end of a commit message. To disable this feature completely configure an empty string like this: `"commit_message": ""` + +The template is parsed using the standard Go [text/templating](http://golang.org/pkg/text/template/), but you probably only need to know that `{{.}}` is replaced by a human readable format of the measured time. \ No newline at end of file From 37f596c43a4b91d7165134ad8b0f30479ee5c3eb Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Mon, 18 May 2015 23:16:07 +0200 Subject: [PATCH 07/28] fixed typos [spent 4m40s] --- docs/config.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/config.md b/docs/config.md index 1a4c69c..e3b94f6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,8 +1,5 @@ # Configuration -Timeglass can be configured by creating a `timeglass.json` file in the root of the repository you are tracking. Timeglass only accepts valid JSON, so if something seems wrong make sure to check your formatting. - -## Example -The following example shows all options with their default configuration: +Timeglass can be configured by creating a `timeglass.json` file in the root of the repository you are tracking. The following example shows all options with their default configuration: ```json { @@ -14,11 +11,11 @@ The following example shows all options with their default configuration: ## MBU __key__: `mbu` -A timer runs in the background and increments by set amount of time eacht tick: the "minimal billable unit". It accepts a human readable format that is parsed by: [time.ParseDuration()](http://golang.org/pkg/time/#ParseDuration) +A timer runs in the background and increments by set amount of time each tick: the "minimal billable unit". It accepts a human readable format that is parsed by: [time.ParseDuration()](http://golang.org/pkg/time/#ParseDuration), e.g: `1h5m2s` ## Commit Message Template __key__: `commit_message` -You can specify how you would like write the time you spent to the end of a commit message. To disable this feature completely configure an empty string like this: `"commit_message": ""` +This options allows you to specify how Timeglass should write spent time to commit messages. To disable this feature completely, provide an empty string, e.g: `"commit_message": ""` -The template is parsed using the standard Go [text/templating](http://golang.org/pkg/text/template/), but you probably only need to know that `{{.}}` is replaced by a human readable format of the measured time. \ No newline at end of file +The template is parsed using the standard Go [text/templating](http://golang.org/pkg/text/template/), but you probably only need to know that `{{.}}` is replaced by a human readable representation of the measured time, e.g: `1h5m2s` \ No newline at end of file From a30fecb15f9637c30ef320824d60c2f6c4510356 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Wed, 20 May 2015 10:11:40 +0200 Subject: [PATCH 08/28] fixed typos in config [spent 9m20s] --- README.md | 2 +- docs/config.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2b8549..8da312a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ __Currently Supported: (see roadmap)__ glass init ``` - _NOTE: you'll only have to run this once per clone_ + _NOTE: you'll have to run this once per clone_ 3. Start the timer by creating a new branch: diff --git a/docs/config.md b/docs/config.md index e3b94f6..90fe43e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -11,7 +11,7 @@ Timeglass can be configured by creating a `timeglass.json` file in the root of t ## MBU __key__: `mbu` -A timer runs in the background and increments by set amount of time each tick: the "minimal billable unit". It accepts a human readable format that is parsed by: [time.ParseDuration()](http://golang.org/pkg/time/#ParseDuration), e.g: `1h5m2s` +A timer runs in the background and increments by a set amount of time each tick: the "minimal billable unit". It accepts a human readable format that is parsed by: [time.ParseDuration()](http://golang.org/pkg/time/#ParseDuration), e.g: `1h5m2s` ## Commit Message Template __key__: `commit_message` From a2b7c08eb5855562af0e64633ddf3057bca9d428 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 13:34:47 +0200 Subject: [PATCH 09/28] configuration file is now actually optional [spent 12m50s] --- model/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/model.go b/model/model.go index 59db710..7d3d727 100644 --- a/model/model.go +++ b/model/model.go @@ -113,7 +113,7 @@ func (m *Model) ReadConfig() (*Config, error) { p := filepath.Join(m.repoDir, "timeglass.json") f, err := os.Open(p) if err != nil { - if err == os.ErrNotExist { + if os.IsNotExist(err) { return conf, nil } From 710877090700e21d70f3254d7a09de81608d79d6 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 14:16:18 +0200 Subject: [PATCH 10/28] added a punch command that writes down spent time on the last commit using git-notes, lap now automatically does this [spent 39m40s] --- command/init.go | 4 +-- command/lap.go | 17 ++++++++++-- command/punch.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + vcs/git.go | 15 +++++++++++ vcs/vcs.go | 2 ++ 6 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 command/punch.go diff --git a/command/init.go b/command/init.go index 7bc0605..1f3513a 100644 --- a/command/init.go +++ b/command/init.go @@ -44,12 +44,12 @@ func (c *Init) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } - vcs, err := vcs.GetVCS(dir) + vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } - err = vcs.Hook() + err = vc.Hook() if err != nil { return errwrap.Wrapf("Failed to write hooks: {{err}}", err) } diff --git a/command/lap.go b/command/lap.go index 81477b2..38c6146 100644 --- a/command/lap.go +++ b/command/lap.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/timeglass/glass/model" + "github.com/timeglass/glass/vcs" ) type Lap struct { @@ -23,11 +24,11 @@ func (c *Lap) Name() string { } func (c *Lap) Description() string { - return fmt.Sprintf("") + return fmt.Sprintf("Resets the running timer, report spent time and punch as time spent on last commit") } func (c *Lap) Usage() string { - return "Show the measured time and reset the timer to 0s" + return "Punch time spent on last commit and reset the timer to 0s" } func (c *Lap) Flags() []cli.Flag { @@ -50,6 +51,7 @@ func (c *Lap) Run(ctx *cli.Context) error { return errwrap.Wrapf(fmt.Sprintf("Failed to get Daemon address: {{err}}"), err) } + //get time and reset client := NewClient(info) t, err := client.Lap() if err != nil { @@ -60,6 +62,17 @@ func (c *Lap) Run(ctx *cli.Context) error { } } + //write the vcs + vc, err := vcs.GetVCS(dir) + if err != nil { + return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) + } + + err = vc.Log(t) + if err != nil { + return errwrap.Wrapf("Failed to log time into VCS: {{err}}", err) + } + fmt.Println(t) return nil } diff --git a/command/punch.go b/command/punch.go new file mode 100644 index 0000000..551a3a8 --- /dev/null +++ b/command/punch.go @@ -0,0 +1,69 @@ +package command + +import ( + "fmt" + "os" + "time" + + "github.com/codegangsta/cli" + "github.com/hashicorp/errwrap" + + "github.com/timeglass/glass/vcs" +) + +type Punch struct { + *command +} + +func NewPunch() *Punch { + return &Punch{newCommand()} +} + +func (c *Punch) Name() string { + return "punch" +} + +func (c *Punch) Description() string { + return fmt.Sprintf("") +} + +func (c *Punch) Usage() string { + return "Manually enter time that was spent on the last commit" +} + +func (c *Punch) Flags() []cli.Flag { + return []cli.Flag{} +} + +func (c *Punch) Action() func(ctx *cli.Context) { + return c.command.Action(c.Run) +} + +func (c *Punch) Run(ctx *cli.Context) error { + dir, err := os.Getwd() + if err != nil { + return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) + } + + if ctx.Args().First() == "" { + return fmt.Errorf("Please provide the time you spent as the first argument") + } + + t, err := time.ParseDuration(ctx.Args().First()) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to parse provided argument '%s' as a valid duration (e.g 1h2m10s): {{err}}", ctx.Args().First()), err) + } + + //write the vcs + vc, err := vcs.GetVCS(dir) + if err != nil { + return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) + } + + err = vc.Log(t) + if err != nil { + return errwrap.Wrapf("Failed to log time into VCS: {{err}}", err) + } + + return nil +} diff --git a/main.go b/main.go index 18afa84..337a2b0 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,7 @@ func main() { command.NewPause(), command.NewStatus(), command.NewLap(), + command.NewPunch(), command.NewStop(), } diff --git a/vcs/git.go b/vcs/git.go index 5a309aa..ae49a0f 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -3,12 +3,16 @@ package vcs import ( "fmt" "os" + "os/exec" "path/filepath" "text/template" + "time" "github.com/hashicorp/errwrap" ) +var TimeSpentNotesRef = "time-spent" + var PostCheckoutTmpl = template.Must(template.New("name").Parse(`#!/bin/sh # when checkout is a branch, start timer if [ $3 -eq 1 ]; then @@ -50,6 +54,17 @@ func (g *Git) Supported() bool { return true } +func (g *Git) Log(t time.Duration) error { + args := []string{"notes", "--ref=" + TimeSpentNotesRef, "add", "-f", "-m", fmt.Sprintf("total=%s", t)} + cmd := exec.Command("git", args...) + err := cmd.Run() + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to log time '%s' using git command %s: {{err}}", t, args), err) + } + + return nil +} + func (g *Git) Hook() error { hpath := filepath.Join(g.dir, "hooks") diff --git a/vcs/vcs.go b/vcs/vcs.go index 52b04e9..b1ceac2 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -3,12 +3,14 @@ package vcs import ( "fmt" "strings" + "time" ) type VCS interface { Name() string Supported() bool Hook() error + Log(time.Duration) error } func GetVCS(dir string) (VCS, error) { From d010ef20cc5aaf98a3aab40ba57ffd4f04eabe6a Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 14:55:53 +0200 Subject: [PATCH 11/28] implemented push and pull commands [spent 38m30s] --- command/lap.go | 2 +- command/pull.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ command/punch.go | 2 +- command/push.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++ command/stop.go | 2 +- main.go | 2 ++ vcs/git.go | 31 ++++++++++++++++++++++- vcs/vcs.go | 3 +++ 8 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 command/pull.go create mode 100644 command/push.go diff --git a/command/lap.go b/command/lap.go index 38c6146..4ea1f47 100644 --- a/command/lap.go +++ b/command/lap.go @@ -28,7 +28,7 @@ func (c *Lap) Description() string { } func (c *Lap) Usage() string { - return "Punch time spent on last commit and reset the timer to 0s" + return "Register time spent on last commit and reset the timer to 0s" } func (c *Lap) Flags() []cli.Flag { diff --git a/command/pull.go b/command/pull.go new file mode 100644 index 0000000..a4104d8 --- /dev/null +++ b/command/pull.go @@ -0,0 +1,64 @@ +package command + +import ( + "fmt" + "os" + + "github.com/codegangsta/cli" + "github.com/hashicorp/errwrap" + + "github.com/timeglass/glass/vcs" +) + +type Pull struct { + *command +} + +func NewPull() *Pull { + return &Pull{newCommand()} +} + +func (c *Pull) Name() string { + return "pull" +} + +func (c *Pull) Description() string { + return fmt.Sprintf("PUll the Timeglass notes branch from the remote repository. Provide the remote's name as the first argument, if no argument is provided it tries to pull from to the VCS default remote") +} + +func (c *Pull) Usage() string { + return "Pull time data from the remote repository" +} + +func (c *Pull) Flags() []cli.Flag { + return []cli.Flag{} +} + +func (c *Pull) Action() func(ctx *cli.Context) { + return c.command.Action(c.Run) +} + +func (c *Pull) Run(ctx *cli.Context) error { + dir, err := os.Getwd() + if err != nil { + return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) + } + + vc, err := vcs.GetVCS(dir) + if err != nil { + return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) + } + + remote := ctx.Args().First() + if remote == "" { + remote = vc.DefaultRemote() + } + + err = vc.Fetch(remote) + if err != nil { + return errwrap.Wrapf("Failed to pull time data: {{err}}", err) + } + + fmt.Println("Timeglass: Fetched successfully") + return nil +} diff --git a/command/punch.go b/command/punch.go index 551a3a8..b7df5c6 100644 --- a/command/punch.go +++ b/command/punch.go @@ -28,7 +28,7 @@ func (c *Punch) Description() string { } func (c *Punch) Usage() string { - return "Manually enter time that was spent on the last commit" + return "Manually register time spent on the last commit" } func (c *Punch) Flags() []cli.Flag { diff --git a/command/push.go b/command/push.go new file mode 100644 index 0000000..bc1fe54 --- /dev/null +++ b/command/push.go @@ -0,0 +1,63 @@ +package command + +import ( + "fmt" + "os" + + "github.com/codegangsta/cli" + "github.com/hashicorp/errwrap" + + "github.com/timeglass/glass/vcs" +) + +type Push struct { + *command +} + +func NewPush() *Push { + return &Push{newCommand()} +} + +func (c *Push) Name() string { + return "push" +} + +func (c *Push) Description() string { + return fmt.Sprintf("Pushes the Timeglass notes branch to the remote repository. Provide the remote's name as the first argument, if no argument is provided it tries to push to the VCS default remote") +} + +func (c *Push) Usage() string { + return "Push time data to the remote repository" +} + +func (c *Push) Flags() []cli.Flag { + return []cli.Flag{} +} + +func (c *Push) Action() func(ctx *cli.Context) { + return c.command.Action(c.Run) +} + +func (c *Push) Run(ctx *cli.Context) error { + dir, err := os.Getwd() + if err != nil { + return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) + } + + vc, err := vcs.GetVCS(dir) + if err != nil { + return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) + } + + remote := ctx.Args().First() + if remote == "" { + remote = vc.DefaultRemote() + } + + err = vc.Push(remote) + if err != nil { + return errwrap.Wrapf("Failed to push time data: {{err}}", err) + } + + return nil +} diff --git a/command/stop.go b/command/stop.go index d017bfb..503e4b0 100644 --- a/command/stop.go +++ b/command/stop.go @@ -27,7 +27,7 @@ func (c *Stop) Description() string { } func (c *Stop) Usage() string { - return "Stop the timer completely" + return "Shutdown the timer, discarding the current measurement" } func (c *Stop) Flags() []cli.Flag { diff --git a/main.go b/main.go index 337a2b0..f3843a3 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,8 @@ func main() { command.NewLap(), command.NewPunch(), command.NewStop(), + command.NewPush(), + command.NewPull(), } for _, c := range cmds { diff --git a/vcs/git.go b/vcs/git.go index ae49a0f..0f41443 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -44,7 +44,8 @@ func NewGit(dir string) *Git { } } -func (g *Git) Name() string { return "git" } +func (g *Git) DefaultRemote() string { return "origin" } +func (g *Git) Name() string { return "git" } func (g *Git) Supported() bool { fi, err := os.Stat(g.dir) if err != nil || !fi.IsDir() { @@ -65,6 +66,34 @@ func (g *Git) Log(t time.Duration) error { return nil } +func (g *Git) Fetch(remote string) error { + args := []string{"fetch", remote, fmt.Sprintf("refs/notes/%s:refs/notes/%s", TimeSpentNotesRef, TimeSpentNotesRef)} + cmd := exec.Command("git", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to fetch from remote '%s' using git command %s: {{err}}", remote, args), err) + } + + return nil +} + +func (g *Git) Push(remote string) error { + args := []string{"push", remote, fmt.Sprintf("refs/notes/%s", TimeSpentNotesRef)} + cmd := exec.Command("git", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to push to remote '%s' using git command %s: {{err}}", remote, args), err) + } + + return nil +} + func (g *Git) Hook() error { hpath := filepath.Join(g.dir, "hooks") diff --git a/vcs/vcs.go b/vcs/vcs.go index b1ceac2..ed72b53 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -10,6 +10,9 @@ type VCS interface { Name() string Supported() bool Hook() error + Push(string) error + Fetch(string) error + DefaultRemote() string Log(time.Duration) error } From 4671eafbda0483dccedcfe5d61ffd84dd3501821 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 15:15:50 +0200 Subject: [PATCH 12/28] now installs a post-update hook that automatically pushes time data as well [spent 9m20s] --- vcs/git.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/vcs/git.go b/vcs/git.go index 0f41443..9c7ef94 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -34,6 +34,11 @@ var PostCommitTmpl = template.Must(template.New("name").Parse(`#!/bin/sh glass lap `)) +var PostUpdateTmpl = template.Must(template.New("name").Parse(`#!/bin/sh +#push time data after push +glass push +`)) + type Git struct { dir string } @@ -145,5 +150,21 @@ func (g *Git) Hook() error { return errwrap.Wrapf("Failed to run post-commit template: {{err}}", err) } + //post update: push() + postuf, err := os.Create(filepath.Join(hpath, "post-update")) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to create post-update '%s': {{err}}", postchf.Name()), err) + } + + err = postuf.Chmod(0766) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to make post-update file '%s' executable: {{err}}", hpath), err) + } + + err = PostUpdateTmpl.Execute(postuf, struct{}{}) + if err != nil { + return errwrap.Wrapf("Failed to run post-update template: {{err}}", err) + } + return nil } From cc9cad8affdb1b550ae0945d3eccc480860adcfd Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 15:23:45 +0200 Subject: [PATCH 13/28] changed to post-receive for push hook [spent 10m30s] --- vcs/git.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/vcs/git.go b/vcs/git.go index 9c7ef94..9a365db 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -34,7 +34,7 @@ var PostCommitTmpl = template.Must(template.New("name").Parse(`#!/bin/sh glass lap `)) -var PostUpdateTmpl = template.Must(template.New("name").Parse(`#!/bin/sh +var PostReceiveTmpl = template.Must(template.New("name").Parse(`#!/bin/sh #push time data after push glass push `)) @@ -137,7 +137,7 @@ func (g *Git) Hook() error { //post commit: lap() postcof, err := os.Create(filepath.Join(hpath, "post-commit")) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("Failed to create post-commit '%s': {{err}}", postchf.Name()), err) + return errwrap.Wrapf(fmt.Sprintf("Failed to create post-commit '%s': {{err}}", postchf.Name()), err) } err = postcof.Chmod(0766) @@ -150,20 +150,20 @@ func (g *Git) Hook() error { return errwrap.Wrapf("Failed to run post-commit template: {{err}}", err) } - //post update: push() - postuf, err := os.Create(filepath.Join(hpath, "post-update")) + //post receive: push() + postrf, err := os.Create(filepath.Join(hpath, "post-receive")) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("Failed to create post-update '%s': {{err}}", postchf.Name()), err) + return errwrap.Wrapf(fmt.Sprintf("Failed to create post-receive '%s': {{err}}", postchf.Name()), err) } - err = postuf.Chmod(0766) + err = postrf.Chmod(0766) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("Failed to make post-update file '%s' executable: {{err}}", hpath), err) + return errwrap.Wrapf(fmt.Sprintf("Failed to make post-receive file '%s' executable: {{err}}", hpath), err) } - err = PostUpdateTmpl.Execute(postuf, struct{}{}) + err = PostReceiveTmpl.Execute(postrf, struct{}{}) if err != nil { - return errwrap.Wrapf("Failed to run post-update template: {{err}}", err) + return errwrap.Wrapf("Failed to run post-receive template: {{err}}", err) } return nil From d0db2e220fd3e6ab1229aedc796ee52fb633c20b Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 15:27:06 +0200 Subject: [PATCH 14/28] ... [spent 7m0s] --- vcs/git.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcs/git.go b/vcs/git.go index 9a365db..ec7d584 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -36,7 +36,7 @@ glass lap var PostReceiveTmpl = template.Must(template.New("name").Parse(`#!/bin/sh #push time data after push -glass push +echo $PWD `)) type Git struct { From 74883f10bee61791558f881e6c7a72a21cc7496c Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 15:58:19 +0200 Subject: [PATCH 15/28] .. [spent 33m50s] --- command/push.go | 9 +++++++++ vcs/git.go | 18 +++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/command/push.go b/command/push.go index bc1fe54..25d34c7 100644 --- a/command/push.go +++ b/command/push.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "io/ioutil" "os" "github.com/codegangsta/cli" @@ -44,6 +45,13 @@ func (c *Push) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return errwrap.Wrapf("Failed to read from stdin: {{err}}", err) + } + + fmt.Println(string(bytes)) + vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) @@ -54,6 +62,7 @@ func (c *Push) Run(ctx *cli.Context) error { remote = vc.DefaultRemote() } + fmt.Printf("Pushing time-data to remote '%s'...\n", remote) err = vc.Push(remote) if err != nil { return errwrap.Wrapf("Failed to push time data: {{err}}", err) diff --git a/vcs/git.go b/vcs/git.go index ec7d584..8592792 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -34,9 +34,9 @@ var PostCommitTmpl = template.Must(template.New("name").Parse(`#!/bin/sh glass lap `)) -var PostReceiveTmpl = template.Must(template.New("name").Parse(`#!/bin/sh -#push time data after push -echo $PWD +var PrePushTmpl = template.Must(template.New("name").Parse(`#!/bin/sh +#push time data +glass push $1 `)) type Git struct { @@ -151,19 +151,19 @@ func (g *Git) Hook() error { } //post receive: push() - postrf, err := os.Create(filepath.Join(hpath, "post-receive")) + prepushf, err := os.Create(filepath.Join(hpath, "pre-push")) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("Failed to create post-receive '%s': {{err}}", postchf.Name()), err) + return errwrap.Wrapf(fmt.Sprintf("Failed to create pre-push '%s': {{err}}", postchf.Name()), err) } - err = postrf.Chmod(0766) + err = prepushf.Chmod(0766) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("Failed to make post-receive file '%s' executable: {{err}}", hpath), err) + return errwrap.Wrapf(fmt.Sprintf("Failed to make pre-push file '%s' executable: {{err}}", hpath), err) } - err = PostReceiveTmpl.Execute(postrf, struct{}{}) + err = PrePushTmpl.Execute(prepushf, struct{}{}) if err != nil { - return errwrap.Wrapf("Failed to run post-receive template: {{err}}", err) + return errwrap.Wrapf("Failed to run pre-push template: {{err}}", err) } return nil From 754d3091fb83bc0fab647f3000ee467594bb7859 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:08:33 +0200 Subject: [PATCH 16/28] simple pre push command [spent 10m30s] --- command/push.go | 24 +++++++++++++++++------- vcs/git.go | 11 +++++++++-- vcs/vcs.go | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/command/push.go b/command/push.go index 25d34c7..caa6e8a 100644 --- a/command/push.go +++ b/command/push.go @@ -32,7 +32,12 @@ func (c *Push) Usage() string { } func (c *Push) Flags() []cli.Flag { - return []cli.Flag{} + return []cli.Flag{ + cli.BoolFlag{ + Name: "refs-on-stdin", + Usage: "Expect the refs that are pushed on stdin", + }, + } } func (c *Push) Action() func(ctx *cli.Context) { @@ -45,12 +50,17 @@ func (c *Push) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } - bytes, err := ioutil.ReadAll(os.Stdin) - if err != nil { - return errwrap.Wrapf("Failed to read from stdin: {{err}}", err) - } + //hooks require us require us to check the refs that are pushed over stdin + //to prevent inifinte push loop + refs := "" + if ctx.Bool("refs-on-stdin") { + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return errwrap.Wrapf("Failed to read from stdin: {{err}}", err) + } - fmt.Println(string(bytes)) + refs = string(bytes) + } vc, err := vcs.GetVCS(dir) if err != nil { @@ -63,7 +73,7 @@ func (c *Push) Run(ctx *cli.Context) error { } fmt.Printf("Pushing time-data to remote '%s'...\n", remote) - err = vc.Push(remote) + err = vc.Push(remote, refs) if err != nil { return errwrap.Wrapf("Failed to push time data: {{err}}", err) } diff --git a/vcs/git.go b/vcs/git.go index 8592792..3a75e0c 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "text/template" "time" @@ -36,7 +37,7 @@ glass lap var PrePushTmpl = template.Must(template.New("name").Parse(`#!/bin/sh #push time data -glass push $1 +glass push $1 --refs-on-stdin `)) type Git struct { @@ -85,7 +86,13 @@ func (g *Git) Fetch(remote string) error { return nil } -func (g *Git) Push(remote string) error { +func (g *Git) Push(remote string, refs string) error { + + //if time ref is already pushed, dont do it again + if strings.Contains(refs, TimeSpentNotesRef) { + return nil + } + args := []string{"push", remote, fmt.Sprintf("refs/notes/%s", TimeSpentNotesRef)} cmd := exec.Command("git", args...) cmd.Stdout = os.Stdout diff --git a/vcs/vcs.go b/vcs/vcs.go index ed72b53..5f28f12 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -10,7 +10,7 @@ type VCS interface { Name() string Supported() bool Hook() error - Push(string) error + Push(string, string) error Fetch(string) error DefaultRemote() string Log(time.Duration) error From 506424b3d7c60f3046154d8ab3f6619daae11627 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:11:45 +0200 Subject: [PATCH 17/28] ... [spent 3m30s] --- command/push.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/command/push.go b/command/push.go index caa6e8a..e058f5c 100644 --- a/command/push.go +++ b/command/push.go @@ -73,6 +73,10 @@ func (c *Push) Run(ctx *cli.Context) error { } fmt.Printf("Pushing time-data to remote '%s'...\n", remote) + if refs != "" { + fmt.Printf("Explicit refs: %s\n", refs) + } + err = vc.Push(remote, refs) if err != nil { return errwrap.Wrapf("Failed to push time data: {{err}}", err) From 24798f84c344a88fbab72ff7e7e80586ef666c1a Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:18:49 +0200 Subject: [PATCH 18/28] working version [spent 7m0s] --- command/push.go | 11 +++++++---- vcs/git.go | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/command/push.go b/command/push.go index e058f5c..35bd1c9 100644 --- a/command/push.go +++ b/command/push.go @@ -60,6 +60,13 @@ func (c *Push) Run(ctx *cli.Context) error { } refs = string(bytes) + //when `glass push` triggers the pre-push hook it will not + //provide any refs on stdin + //this probalby means means there is nothing left to push and + //we return here to prevent recursive push + if refs == "" { + return nil + } } vc, err := vcs.GetVCS(dir) @@ -73,10 +80,6 @@ func (c *Push) Run(ctx *cli.Context) error { } fmt.Printf("Pushing time-data to remote '%s'...\n", remote) - if refs != "" { - fmt.Printf("Explicit refs: %s\n", refs) - } - err = vc.Push(remote, refs) if err != nil { return errwrap.Wrapf("Failed to push time data: {{err}}", err) diff --git a/vcs/git.go b/vcs/git.go index 3a75e0c..41d73a6 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -37,6 +37,7 @@ glass lap var PrePushTmpl = template.Must(template.New("name").Parse(`#!/bin/sh #push time data +echo Hook $1 $2 glass push $1 --refs-on-stdin `)) From c51c695a5403a311e81bda4ad96217e1a4daefe1 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:25:34 +0200 Subject: [PATCH 19/28] renamed flag to something more descriptive [spent 5m50s] --- command/push.go | 6 +++--- vcs/git.go | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/command/push.go b/command/push.go index 35bd1c9..50dd8a2 100644 --- a/command/push.go +++ b/command/push.go @@ -34,8 +34,8 @@ func (c *Push) Usage() string { func (c *Push) Flags() []cli.Flag { return []cli.Flag{ cli.BoolFlag{ - Name: "refs-on-stdin", - Usage: "Expect the refs that are pushed on stdin", + Name: "from-hook", + Usage: "Indicate it is called from a git, now expects refs on stdin", }, } } @@ -53,7 +53,7 @@ func (c *Push) Run(ctx *cli.Context) error { //hooks require us require us to check the refs that are pushed over stdin //to prevent inifinte push loop refs := "" - if ctx.Bool("refs-on-stdin") { + if ctx.Bool("from-hook") { bytes, err := ioutil.ReadAll(os.Stdin) if err != nil { return errwrap.Wrapf("Failed to read from stdin: {{err}}", err) diff --git a/vcs/git.go b/vcs/git.go index 41d73a6..314a496 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -37,8 +37,7 @@ glass lap var PrePushTmpl = template.Must(template.New("name").Parse(`#!/bin/sh #push time data -echo Hook $1 $2 -glass push $1 --refs-on-stdin +glass push $1 --from-hook `)) type Git struct { From 9277a0d5c4c2b66f7a27110e5a09e13acb6c17aa Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:26:44 +0200 Subject: [PATCH 20/28] removed log message about pushing as at that point its not actually clear wether something will be pushed [spent 1m10s] --- command/push.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/push.go b/command/push.go index 50dd8a2..20c8843 100644 --- a/command/push.go +++ b/command/push.go @@ -79,7 +79,6 @@ func (c *Push) Run(ctx *cli.Context) error { remote = vc.DefaultRemote() } - fmt.Printf("Pushing time-data to remote '%s'...\n", remote) err = vc.Push(remote, refs) if err != nil { return errwrap.Wrapf("Failed to push time data: {{err}}", err) From 71516c8611b0e55f079df801df25e46ff8875a1f Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:30:19 +0200 Subject: [PATCH 21/28] configurable auto_push --- command/push.go | 13 +++++++++++++ model/config.go | 2 ++ timeglass.json | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/command/push.go b/command/push.go index 20c8843..fc661c0 100644 --- a/command/push.go +++ b/command/push.go @@ -8,6 +8,7 @@ import ( "github.com/codegangsta/cli" "github.com/hashicorp/errwrap" + "github.com/timeglass/glass/model" "github.com/timeglass/glass/vcs" ) @@ -50,6 +51,12 @@ func (c *Push) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } + m := model.New(dir) + conf, err := m.ReadConfig() + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Failed to read configuration: {{err}}"), err) + } + //hooks require us require us to check the refs that are pushed over stdin //to prevent inifinte push loop refs := "" @@ -67,6 +74,12 @@ func (c *Push) Run(ctx *cli.Context) error { if refs == "" { return nil } + + //configuration can explicitly request not to push time data automatically + //on hook usage + if !conf.AutoPush { + return nil + } } vc, err := vcs.GetVCS(dir) diff --git a/model/config.go b/model/config.go index 18c70b9..1d0e7ff 100644 --- a/model/config.go +++ b/model/config.go @@ -29,9 +29,11 @@ func (t *MBU) UnmarshalJSON(data []byte) error { var DefaultConfig = &Config{ MBU: MBU(time.Minute), CommitMessage: " [{{.}}]", + AutoPush: true, } type Config struct { MBU MBU `json:"mbu"` CommitMessage string `json:"commit_message"` + AutoPush bool `json:"auto_push"` } diff --git a/timeglass.json b/timeglass.json index 1be9e74..5d2c5d6 100644 --- a/timeglass.json +++ b/timeglass.json @@ -1,4 +1,5 @@ { "mbu": "1m10s", - "commit_message": " [spent {{.}}]" + "commit_message": " [spent {{.}}]", + "auto_push": false, } \ No newline at end of file From 6e2577ff8664e0defae3740bfab912c96eebefd6 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:31:22 +0200 Subject: [PATCH 22/28] configure with auto_push --- timeglass.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timeglass.json b/timeglass.json index 5d2c5d6..9ae502d 100644 --- a/timeglass.json +++ b/timeglass.json @@ -1,5 +1,5 @@ { "mbu": "1m10s", "commit_message": " [spent {{.}}]", - "auto_push": false, + "auto_push": true } \ No newline at end of file From a485eeaa295a24041b948143ccff470490683c8f Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:37:45 +0200 Subject: [PATCH 23/28] documented auto_push [spent 7m0s] --- docs/config.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index 90fe43e..9cabeca 100644 --- a/docs/config.md +++ b/docs/config.md @@ -4,7 +4,8 @@ Timeglass can be configured by creating a `timeglass.json` file in the root of t ```json { "mbu": "1m", - "commit_message": " [{{.}}]" + "commit_message": " [{{.}}]", + "auto_push": true } ``` @@ -18,4 +19,9 @@ __key__: `commit_message` This options allows you to specify how Timeglass should write spent time to commit messages. To disable this feature completely, provide an empty string, e.g: `"commit_message": ""` -The template is parsed using the standard Go [text/templating](http://golang.org/pkg/text/template/), but you probably only need to know that `{{.}}` is replaced by a human readable representation of the measured time, e.g: `1h5m2s` \ No newline at end of file +The template is parsed using the standard Go [text/templating](http://golang.org/pkg/text/template/), but you probably only need to know that `{{.}}` is replaced by a human readable representation of the measured time, e.g: `1h5m2s` + +## Automatically Push Time data +__key__: `auto_push` + +Timeglass uses [git-notes](http://git-scm.com/docs/git-notes) for storing commit times, since git-notes uses a seperate branch for such data it needs to be explicitely pushed or else data is merely stored local and lost whenever the clone is removed. To prevent this, Timeglass installes a pre-push hook that automatically pushes time data to the same remote as the push itself. If you rather want full control over when to push time data using the `glass push` command, you can disable the automatic behaviour with this options: `"auto_push": false` \ No newline at end of file From a8130207a3050479bab08b167102683624aaa6fa Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 16:56:31 +0200 Subject: [PATCH 24/28] glass init now immediately pulls remote time data [spent 18m40s] --- README.md | 4 +++- command/init.go | 2 +- command/pull.go | 2 +- docs/config.md | 5 +++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8da312a..83e94d8 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,10 @@ __Features:__ - The timer __automatically starts__ when you switch to a (new) branch using `git checkout` - The timer __automatically pauses__ when it doesn't detect any file activity for a while -- The time you spent is automatically added to the next `git commit` message +- The time you spent is automatically added to the next `git commit` message by default - The timer increments in discreet steps: the _minimal billable unit_ (MBU), by default this is 1m. +- Spent time is stored as commit metadata using [git-notes](git-scm.com/docs/git-notes) and pushed automatically to your remote by default + __Currently Supported: (see roadmap)__ diff --git a/command/init.go b/command/init.go index 1f3513a..42145aa 100644 --- a/command/init.go +++ b/command/init.go @@ -55,5 +55,5 @@ func (c *Init) Run(ctx *cli.Context) error { } fmt.Println("Timeglass: hooks written") - return nil + return NewPull().Run(ctx) } diff --git a/command/pull.go b/command/pull.go index a4104d8..ce64b36 100644 --- a/command/pull.go +++ b/command/pull.go @@ -59,6 +59,6 @@ func (c *Pull) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to pull time data: {{err}}", err) } - fmt.Println("Timeglass: Fetched successfully") + fmt.Println("Timeglass: time data fetched successfully") return nil } diff --git a/docs/config.md b/docs/config.md index 9cabeca..c5a87f9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -22,6 +22,7 @@ This options allows you to specify how Timeglass should write spent time to comm The template is parsed using the standard Go [text/templating](http://golang.org/pkg/text/template/), but you probably only need to know that `{{.}}` is replaced by a human readable representation of the measured time, e.g: `1h5m2s` ## Automatically Push Time data -__key__: `auto_push` +__key__: `auto_push` +__requirements__: git v1.8.2.1 or higher -Timeglass uses [git-notes](http://git-scm.com/docs/git-notes) for storing commit times, since git-notes uses a seperate branch for such data it needs to be explicitely pushed or else data is merely stored local and lost whenever the clone is removed. To prevent this, Timeglass installes a pre-push hook that automatically pushes time data to the same remote as the push itself. If you rather want full control over when to push time data using the `glass push` command, you can disable the automatic behaviour with this options: `"auto_push": false` \ No newline at end of file +Timeglass uses [git-notes](http://git-scm.com/docs/git-notes) for storing commit times, since git-notes uses a seperate branch for such data it needs to be explicitely pushed or else data is merely stored local and lost whenever the clone is removed. To prevent this, Timeglass installes a pre-push hook that automatically pushes time data to the same remote as the push itself. If you rather want full control over when to push time data using the `glass push` command, you can disable the automatic behaviour with this options: `"auto_push": false`. The pre-push hook was introduced in git v1.8.2, if you're running an older version the hook is simply not run and this option does nothing. \ No newline at end of file From 2771576bd9e0745f38f826cc31b1f8ddc04f967b Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 17:17:59 +0200 Subject: [PATCH 25/28] no scary error messages when users try to push/pull from and to places without the time-spend branch [spent 21m0s] --- command/pull.go | 5 +++++ command/push.go | 5 +++++ vcs/git.go | 28 ++++++++++++++++++++++++++-- vcs/vcs.go | 4 ++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/command/pull.go b/command/pull.go index ce64b36..7ae7efa 100644 --- a/command/pull.go +++ b/command/pull.go @@ -56,6 +56,11 @@ func (c *Pull) Run(ctx *cli.Context) error { err = vc.Fetch(remote) if err != nil { + if err == vcs.ErrNoRemoteTimeData { + fmt.Printf("Timeglass: remote '%s' has no time data (yet), nothing to pull\n", remote) + return nil + } + return errwrap.Wrapf("Failed to pull time data: {{err}}", err) } diff --git a/command/push.go b/command/push.go index fc661c0..851a5ae 100644 --- a/command/push.go +++ b/command/push.go @@ -94,6 +94,11 @@ func (c *Push) Run(ctx *cli.Context) error { err = vc.Push(remote, refs) if err != nil { + if err == vcs.ErrNoLocalTimeData { + fmt.Printf("Timeglass: local clone has no time data (yet), nothing to push to '%s'. Start a timer and commit changes to record local time data.\n", remote) + return nil + } + return errwrap.Wrapf("Failed to push time data: {{err}}", err) } diff --git a/vcs/git.go b/vcs/git.go index 314a496..7f2f42c 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -1,7 +1,9 @@ package vcs import ( + "bytes" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -75,11 +77,22 @@ func (g *Git) Log(t time.Duration) error { func (g *Git) Fetch(remote string) error { args := []string{"fetch", remote, fmt.Sprintf("refs/notes/%s:refs/notes/%s", TimeSpentNotesRef, TimeSpentNotesRef)} cmd := exec.Command("git", args...) + buff := bytes.NewBuffer(nil) + cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + cmd.Stderr = buff err := cmd.Run() if err != nil { + if strings.Contains(buff.String(), "Couldn't find remote ref") { + return ErrNoRemoteTimeData + } + + _, err2 := io.Copy(os.Stderr, buff) + if err2 != nil { + return err + } + return errwrap.Wrapf(fmt.Sprintf("Failed to fetch from remote '%s' using git command %s: {{err}}", remote, args), err) } @@ -95,11 +108,22 @@ func (g *Git) Push(remote string, refs string) error { args := []string{"push", remote, fmt.Sprintf("refs/notes/%s", TimeSpentNotesRef)} cmd := exec.Command("git", args...) + buff := bytes.NewBuffer(nil) + cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + cmd.Stderr = buff err := cmd.Run() if err != nil { + if strings.Contains(buff.String(), "src refspec refs/notes/"+TimeSpentNotesRef+" does not match any") { + return ErrNoLocalTimeData + } + + _, err2 := io.Copy(os.Stderr, buff) + if err2 != nil { + return err + } + return errwrap.Wrapf(fmt.Sprintf("Failed to push to remote '%s' using git command %s: {{err}}", remote, args), err) } diff --git a/vcs/vcs.go b/vcs/vcs.go index 5f28f12..b0d2ca4 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -1,11 +1,15 @@ package vcs import ( + "errors" "fmt" "strings" "time" ) +var ErrNoRemoteTimeData = errors.New("Remote doesn't have any time data") +var ErrNoLocalTimeData = errors.New("Local clone doesn't have any time data") + type VCS interface { Name() string Supported() bool From 933971af8abe3a32e98715e48d5665b2890e3a80 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 17:21:20 +0200 Subject: [PATCH 26/28] more feedback for glass push [spent 3m30s] --- command/push.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/push.go b/command/push.go index 851a5ae..a636971 100644 --- a/command/push.go +++ b/command/push.go @@ -102,5 +102,6 @@ func (c *Push) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to push time data: {{err}}", err) } + fmt.Println("Timeglass: time data pushed successfully") return nil } From fd5bef75d94f9d01073fbbb30044bd32eb9dc8c0 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 17:25:31 +0200 Subject: [PATCH 27/28] only hide stderr when we know the error is no problem [spent 3m30s] --- command/push.go | 1 - vcs/git.go | 34 ++++++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/command/push.go b/command/push.go index a636971..851a5ae 100644 --- a/command/push.go +++ b/command/push.go @@ -102,6 +102,5 @@ func (c *Push) Run(ctx *cli.Context) error { return errwrap.Wrapf("Failed to push time data: {{err}}", err) } - fmt.Println("Timeglass: time data pushed successfully") return nil } diff --git a/vcs/git.go b/vcs/git.go index 7f2f42c..279fec5 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -83,16 +83,17 @@ func (g *Git) Fetch(remote string) error { cmd.Stderr = buff err := cmd.Run() - if err != nil { - if strings.Contains(buff.String(), "Couldn't find remote ref") { - return ErrNoRemoteTimeData - } + if err != nil && strings.Contains(buff.String(), "Couldn't find remote ref") { + return ErrNoRemoteTimeData + } - _, err2 := io.Copy(os.Stderr, buff) - if err2 != nil { - return err - } + //in other cases present user with git output + _, err2 := io.Copy(os.Stderr, buff) + if err2 != nil { + return err + } + if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to fetch from remote '%s' using git command %s: {{err}}", remote, args), err) } @@ -114,16 +115,17 @@ func (g *Git) Push(remote string, refs string) error { cmd.Stderr = buff err := cmd.Run() - if err != nil { - if strings.Contains(buff.String(), "src refspec refs/notes/"+TimeSpentNotesRef+" does not match any") { - return ErrNoLocalTimeData - } + if err != nil && strings.Contains(buff.String(), "src refspec refs/notes/"+TimeSpentNotesRef+" does not match any") { + return ErrNoLocalTimeData + } - _, err2 := io.Copy(os.Stderr, buff) - if err2 != nil { - return err - } + //in other cases present user with git output + _, err2 := io.Copy(os.Stderr, buff) + if err2 != nil { + return err + } + if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to push to remote '%s' using git command %s: {{err}}", remote, args), err) } From 2d5676507cfe4bd9c6ae35a2244b0e6a2895a344 Mon Sep 17 00:00:00 2001 From: Ad van der Veer Date: Fri, 22 May 2015 17:57:41 +0200 Subject: [PATCH 28/28] no longer show nasty error messages on commit whem daemon is not running, fixes #5 [spent 22m10s] --- command/lap.go | 12 +++++++++++- command/pull.go | 2 +- command/status.go | 5 +++++ command/types.go | 2 +- vcs/git.go | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/command/lap.go b/command/lap.go index 4ea1f47..d3a1a4d 100644 --- a/command/lap.go +++ b/command/lap.go @@ -32,7 +32,12 @@ func (c *Lap) Usage() string { } func (c *Lap) Flags() []cli.Flag { - return []cli.Flag{} + return []cli.Flag{ + cli.BoolFlag{ + Name: "from-hook", + Usage: "Indicate it is called from a git, now expects refs on stdin", + }, + } } func (c *Lap) Action() func(ctx *cli.Context) { @@ -56,6 +61,11 @@ func (c *Lap) Run(ctx *cli.Context) error { t, err := client.Lap() if err != nil { if err == ErrDaemonDown { + //if were calling this from a hook, supress errors + if ctx.Bool("from-hook") { + return nil + } + return errwrap.Wrapf(fmt.Sprintf("No timer appears to be running for '%s': {{err}}", dir), err) } else { return err diff --git a/command/pull.go b/command/pull.go index 7ae7efa..0aa9e94 100644 --- a/command/pull.go +++ b/command/pull.go @@ -23,7 +23,7 @@ func (c *Pull) Name() string { } func (c *Pull) Description() string { - return fmt.Sprintf("PUll the Timeglass notes branch from the remote repository. Provide the remote's name as the first argument, if no argument is provided it tries to pull from to the VCS default remote") + return fmt.Sprintf("Pull the Timeglass notes branch from the remote repository. Provide the remote's name as the first argument, if no argument is provided it tries to pull from to the VCS default remote") } func (c *Pull) Usage() string { diff --git a/command/status.go b/command/status.go index 56ebbe2..6e73bbe 100644 --- a/command/status.go +++ b/command/status.go @@ -72,6 +72,11 @@ func (c *Status) Run(ctx *cli.Context) error { status, err := client.GetStatus() if err != nil { if err == ErrDaemonDown { + //if called from hook, don't interrupt + if ctx.Bool("time-only") { + return nil + } + return errwrap.Wrapf(fmt.Sprintf("No timer appears to be running for '%s': {{err}}", dir), err) } else { return err diff --git a/command/types.go b/command/types.go index 6caf3d9..63879ec 100644 --- a/command/types.go +++ b/command/types.go @@ -21,7 +21,7 @@ func (c *command) Action(fn func(c *cli.Context) error) func(ctx *cli.Context) { return func(ctx *cli.Context) { err := fn(ctx) if err != nil { - c.Fatalf("[Error]: %s", err) + c.Fatalf("[Timeglass Error]: %s", err) return } } diff --git a/vcs/git.go b/vcs/git.go index 279fec5..551e927 100644 --- a/vcs/git.go +++ b/vcs/git.go @@ -34,7 +34,7 @@ esac var PostCommitTmpl = template.Must(template.New("name").Parse(`#!/bin/sh #always reset after commit -glass lap +glass lap --from-hook `)) var PrePushTmpl = template.Must(template.New("name").Parse(`#!/bin/sh