github.com/kilpkonn/gtm-enhanced@v1.3.5/project/project.go (about)

     1  // Copyright 2016 Michael Schenk. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package project
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"runtime"
    16  	"strings"
    17  	"text/template"
    18  
    19  	"github.com/git-time-metric/gtm/scm"
    20  	"github.com/git-time-metric/gtm/util"
    21  	"github.com/mattn/go-isatty"
    22  )
    23  
    24  var (
    25  	// ErrNotInitialized is raised when a git repo not initialized for time tracking
    26  	ErrNotInitialized = errors.New("Git Time Metric is not initialized")
    27  	// ErrFileNotFound is raised when record an event for a file that does not exist
    28  	ErrFileNotFound = errors.New("File does not exist")
    29  )
    30  
    31  var (
    32  	// GitHooks is map of hooks to apply to the git repo
    33  	GitHooks = map[string]scm.GitHook{
    34  		"post-commit": {
    35  			Exe:     "gtm",
    36  			Command: "gtm commit --yes",
    37  			RE:      regexp.MustCompile(`(?s)[/:a-zA-Z0-9$_=()"\.\|\-\\ ]*gtm(.exe"|)\s+commit\s+--yes\.*`)},
    38  	}
    39  	// GitConfig is map of git configuration settings
    40  	GitConfig = map[string]string{
    41  		"alias.pushgtm":    "push origin refs/notes/gtm-data",
    42  		"alias.fetchgtm":   "fetch origin refs/notes/gtm-data:refs/notes/gtm-data",
    43  		"notes.rewriteref": "refs/notes/gtm-data"}
    44  	// GitIgnore is file ignore to apply to git repo
    45  	GitIgnore = "/.gtm/"
    46  )
    47  
    48  const (
    49  	// NoteNameSpace is the gtm git note namespace
    50  	NoteNameSpace = "gtm-data"
    51  	// GTMDir is the subdir for gtm within the git repo root directory
    52  	GTMDir = ".gtm"
    53  )
    54  
    55  const initMsgTpl string = `
    56  {{print "Git Time Metric initialized for " (.ProjectPath) | printf (.HeaderFormat) }}
    57  
    58  {{ range $hook, $command := .GitHooks -}}
    59  	{{- $hook | printf "%16s" }}: {{ $command.Command }}
    60  {{ end -}}
    61  {{ range $key, $val := .GitConfig -}}
    62  	{{- $key | printf "%16s" }}: {{ $val }}
    63  {{end -}}
    64  {{ print "terminal:" | printf "%17s" }} {{ .Terminal }}
    65  {{ print ".gitignore:" | printf "%17s" }} {{ .GitIgnore }}
    66  {{ print "tags:" | printf "%17s" }} {{.Tags }}
    67  `
    68  const removeMsgTpl string = `
    69  {{print "Git Time Metric uninitialized for " (.ProjectPath) | printf (.HeaderFormat) }}
    70  
    71  The following items have been removed.
    72  
    73  {{ range $hook, $command := .GitHooks -}}
    74  	{{- $hook | printf "%16s" }}: {{ $command.Command }}
    75  {{ end -}}
    76  {{ range $key, $val := .GitConfig -}}
    77  	{{- $key | printf "%16s" }}: {{ $val }}
    78  {{end -}}
    79  {{ print ".gitignore:" | printf "%17s" }} {{ .GitIgnore }}
    80  `
    81  
    82  // Initialize initializes a git repo for time tracking
    83  func Initialize(terminal bool, tags []string, clearTags bool) (string, error) {
    84  	wd, err := os.Getwd()
    85  
    86  	if err != nil {
    87  		return "", err
    88  	}
    89  
    90  	gitRepoPath, err := scm.GitRepoPath(wd)
    91  	if err != nil {
    92  		return "", fmt.Errorf(
    93  			"Unable to intialize Git Time Metric, Git repository not found in '%s'", gitRepoPath)
    94  	}
    95  	if _, err := os.Stat(gitRepoPath); os.IsNotExist(err) {
    96  		return "", fmt.Errorf(
    97  			"Unable to intialize Git Time Metric, Git repository not found in %s", gitRepoPath)
    98  	}
    99  
   100  	workDirRoot, err := scm.Workdir(gitRepoPath)
   101  	if err != nil {
   102  		return "", fmt.Errorf(
   103  			"Unable to intialize Git Time Metric, Git working tree root not found in %s", workDirRoot)
   104  
   105  	}
   106  
   107  	if _, err := os.Stat(workDirRoot); os.IsNotExist(err) {
   108  		return "", fmt.Errorf(
   109  			"Unable to intialize Git Time Metric, Git working tree root not found in %s", workDirRoot)
   110  	}
   111  
   112  	gtmPath := filepath.Join(workDirRoot, GTMDir)
   113  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   114  		if err := os.MkdirAll(gtmPath, 0700); err != nil {
   115  			return "", err
   116  		}
   117  	}
   118  
   119  	if clearTags {
   120  		err = removeTags(gtmPath)
   121  		if err != nil {
   122  			return "", err
   123  		}
   124  	}
   125  	err = saveTags(tags, gtmPath)
   126  	if err != nil {
   127  		return "", err
   128  	}
   129  	tags, err = LoadTags(gtmPath)
   130  	if err != nil {
   131  		return "", err
   132  	}
   133  
   134  	if terminal {
   135  		if err := ioutil.WriteFile(filepath.Join(gtmPath, "terminal.app"), []byte(""), 0644); err != nil {
   136  			return "", err
   137  		}
   138  	} else {
   139  		// try to remove terminal.app, it may not exist
   140  		_ = os.Remove(filepath.Join(gtmPath, "terminal.app"))
   141  	}
   142  
   143  	if err := scm.SetHooks(GitHooks, gitRepoPath); err != nil {
   144  		return "", err
   145  	}
   146  
   147  	if err := scm.ConfigSet(GitConfig, gitRepoPath); err != nil {
   148  		return "", err
   149  	}
   150  
   151  	if err := scm.IgnoreSet(GitIgnore, workDirRoot); err != nil {
   152  		return "", err
   153  	}
   154  
   155  	headerFormat := "%s"
   156  	if isatty.IsTerminal(os.Stdout.Fd()) && runtime.GOOS != "windows" {
   157  		headerFormat = "\x1b[1m%s\x1b[0m"
   158  	}
   159  
   160  	b := new(bytes.Buffer)
   161  	t := template.Must(template.New("msg").Parse(initMsgTpl))
   162  	err = t.Execute(b,
   163  		struct {
   164  			Tags         string
   165  			HeaderFormat string
   166  			ProjectPath  string
   167  			GitHooks     map[string]scm.GitHook
   168  			GitConfig    map[string]string
   169  			GitIgnore    string
   170  			Terminal     bool
   171  		}{
   172  			strings.Join(tags, " "),
   173  			headerFormat,
   174  			workDirRoot,
   175  			GitHooks,
   176  			GitConfig,
   177  			GitIgnore,
   178  			terminal,
   179  		})
   180  
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  
   185  	index, err := NewIndex()
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  
   190  	index.add(workDirRoot)
   191  	err = index.save()
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  
   196  	return b.String(), nil
   197  }
   198  
   199  //Uninitialize remove GTM tracking from the project in the current working directory
   200  func Uninitialize() (string, error) {
   201  	wd, err := os.Getwd()
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  
   206  	gitRepoPath, err := scm.GitRepoPath(wd)
   207  	if err != nil {
   208  		return "", fmt.Errorf(
   209  			"Unable to unintialize Git Time Metric, Git repository not found in %s", gitRepoPath)
   210  	}
   211  
   212  	workDir, _ := scm.Workdir(gitRepoPath)
   213  	gtmPath := filepath.Join(workDir, GTMDir)
   214  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   215  		return "", fmt.Errorf(
   216  			"Unable to uninitialize Git Time Metric, %s directory not found", gtmPath)
   217  	}
   218  	if err := scm.RemoveHooks(GitHooks, gitRepoPath); err != nil {
   219  		return "", err
   220  	}
   221  	if err := scm.ConfigRemove(GitConfig, gitRepoPath); err != nil {
   222  		return "", err
   223  	}
   224  	if err := scm.IgnoreRemove(GitIgnore, workDir); err != nil {
   225  		return "", err
   226  	}
   227  	if err := os.RemoveAll(gtmPath); err != nil {
   228  		return "", err
   229  	}
   230  
   231  	headerFormat := "%s"
   232  	if isatty.IsTerminal(os.Stdout.Fd()) && runtime.GOOS != "windows" {
   233  		headerFormat = "\x1b[1m%s\x1b[0m"
   234  	}
   235  	b := new(bytes.Buffer)
   236  	t := template.Must(template.New("msg").Parse(removeMsgTpl))
   237  	err = t.Execute(b,
   238  		struct {
   239  			HeaderFormat string
   240  			ProjectPath  string
   241  			GitHooks     map[string]scm.GitHook
   242  			GitConfig    map[string]string
   243  			GitIgnore    string
   244  		}{
   245  			headerFormat,
   246  			workDir,
   247  			GitHooks,
   248  			GitConfig,
   249  			GitIgnore})
   250  
   251  	if err != nil {
   252  		return "", err
   253  	}
   254  
   255  	index, err := NewIndex()
   256  	if err != nil {
   257  		return "", err
   258  	}
   259  
   260  	index.remove(workDir)
   261  	err = index.save()
   262  	if err != nil {
   263  		return "", err
   264  	}
   265  
   266  	return b.String(), nil
   267  }
   268  
   269  //Clean removes any event or metrics files from project in the current working directory
   270  func Clean(dr util.DateRange, terminalOnly bool) error {
   271  	wd, err := os.Getwd()
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	gitRepoPath, err := scm.GitRepoPath(wd)
   277  	if err != nil {
   278  		return fmt.Errorf("Unable to clean, Git repository not found in %s", gitRepoPath)
   279  	}
   280  
   281  	workDir, err := scm.Workdir(gitRepoPath)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	gtmPath := filepath.Join(workDir, GTMDir)
   287  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   288  		return fmt.Errorf("Unable to clean GTM data, %s directory not found", gtmPath)
   289  	}
   290  
   291  	files, err := ioutil.ReadDir(gtmPath)
   292  	if err != nil {
   293  		return err
   294  	}
   295  	for _, f := range files {
   296  		if !strings.HasSuffix(f.Name(), ".event") &&
   297  			!strings.HasSuffix(f.Name(), ".metric") {
   298  			continue
   299  		}
   300  		if !dr.Within(f.ModTime()) {
   301  			continue
   302  		}
   303  		fp := filepath.Join(gtmPath, f.Name())
   304  		if terminalOnly && strings.HasSuffix(f.Name(), ".event") {
   305  			b, err := ioutil.ReadFile(fp)
   306  			if err != nil {
   307  				return err
   308  			}
   309  			if !strings.Contains(string(b), "terminal.app") {
   310  				continue
   311  			}
   312  		}
   313  		if err := os.Remove(fp); err != nil {
   314  			return err
   315  		}
   316  	}
   317  	return nil
   318  }
   319  
   320  // Paths returns the root git repo and gtm paths
   321  func Paths(wd ...string) (string, string, error) {
   322  	defer util.Profile()()
   323  
   324  	var (
   325  		gitRepoPath string
   326  		err         error
   327  	)
   328  	if len(wd) > 0 {
   329  		gitRepoPath, err = scm.GitRepoPath(wd[0])
   330  	} else {
   331  		gitRepoPath, err = scm.GitRepoPath()
   332  	}
   333  	if err != nil {
   334  		return "", "", ErrNotInitialized
   335  	}
   336  
   337  	workDir, err := scm.Workdir(gitRepoPath)
   338  	if err != nil {
   339  		return "", "", ErrNotInitialized
   340  	}
   341  
   342  	gtmPath := filepath.Join(workDir, GTMDir)
   343  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   344  		return "", "", ErrNotInitialized
   345  	}
   346  	return workDir, gtmPath, nil
   347  }
   348  
   349  func removeTags(gtmPath string) error {
   350  	files, err := ioutil.ReadDir(gtmPath)
   351  	if err != nil {
   352  		return err
   353  	}
   354  	for i := range files {
   355  		if strings.HasSuffix(files[i].Name(), ".tag") {
   356  			tagFile := filepath.Join(gtmPath, files[i].Name())
   357  			if err := os.Remove(tagFile); err != nil {
   358  				return err
   359  			}
   360  		}
   361  	}
   362  	return nil
   363  }
   364  
   365  // LoadTags returns the tags for the project in the gtmPath directory
   366  func LoadTags(gtmPath string) ([]string, error) {
   367  	tags := []string{}
   368  	files, err := ioutil.ReadDir(gtmPath)
   369  	if err != nil {
   370  		return []string{}, err
   371  	}
   372  	for i := range files {
   373  		if strings.HasSuffix(files[i].Name(), ".tag") {
   374  			tags = append(tags, strings.TrimSuffix(files[i].Name(), filepath.Ext(files[i].Name())))
   375  		}
   376  	}
   377  	return tags, nil
   378  }
   379  
   380  func saveTags(tags []string, gtmPath string) error {
   381  	if len(tags) > 0 {
   382  		for _, t := range tags {
   383  			if strings.TrimSpace(t) == "" {
   384  				continue
   385  			}
   386  			if err := ioutil.WriteFile(filepath.Join(gtmPath, fmt.Sprintf("%s.tag", t)), []byte(""), 0644); err != nil {
   387  				return err
   388  			}
   389  		}
   390  	}
   391  	return nil
   392  }