github.com/developest/gtm-core@v1.0.4-0.20220111132249-cc80a3372c3f/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/DEVELOPEST/gtm-core/scm"
    20  	"github.com/DEVELOPEST/gtm-core/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  	// AppEventFileContentRegex regex for app event files
    30  	AppEventFileContentRegex = regexp.MustCompile(`\.gtm[\\/](?P<appName>.*)\.(?P<eventType>app|run|build)`)
    31  )
    32  
    33  var (
    34  	// GitHooks is map of hooks to apply to the git repo
    35  	GitHooks = map[string]scm.GitHook{
    36  		"post-commit": {
    37  			Exe:     "gtm",
    38  			Command: "gtm commit --yes",
    39  			RE:      regexp.MustCompile(`(?s)[/:a-zA-Z0-9$_=()"\.\|\-\\ ]*gtm(.exe"|)\s+commit\s+--yes\.*`),
    40  		},
    41  		// "post-rewrite": {
    42  		// 	Exe:     "gtm",
    43  		// 	Command: "gtm rewrite",
    44  		// 	RE: regexp.MustCompile(
    45  		// 		`(?s)[/:a-zA-Z0-9$_=()"\.\|\-\\ ]*gtm\s+rewrite\.*`),
    46  		// },
    47  	}
    48  	// GitConfig is map of git configuration settings
    49  	GitConfig = map[string]string{
    50  		"alias.pushgtm":        "push origin refs/notes/gtm-data",
    51  		"alias.fetchgtm":       "fetch origin refs/notes/gtm-data:refs/notes/gtm-data",
    52  		"notes.rewriteRef":     "refs/notes/gtm-data",
    53  		"notes.rewriteMode":    "concatenate",
    54  		"notes.rewrite.rebase": "true",
    55  		"notes.rewrite.amend":  "true"}
    56  	// GitIgnore is file ignore to apply to git repo
    57  	GitIgnore = "/.gtm/"
    58  
    59  	GitFetchRefs = []string{
    60  		"+refs/notes/gtm-data:refs/notes/gtm-data",
    61  	}
    62  
    63  	GitPushRefsHooks = map[string]scm.GitHook{
    64  		"pre-push": {
    65  			Exe:     "git",
    66  			Command: "git push origin refs/notes/gtm-data --no-verify",
    67  			RE: regexp.MustCompile(
    68  				`(?s)[/:a-zA-Z0-9$_=()"\.\|\-\\ ]*git\s+push\s+origin\s+refs/notes/gtm-data\s+--no-verify\.*`),
    69  		},
    70  	}
    71  
    72  	GitLabHooks = map[string]scm.GitHook{
    73  		"prepare-commit-msg": {
    74  			Exe:     "gtm",
    75  			Command: "gtm status --auto-log=gitlab >> $1",
    76  			RE: regexp.MustCompile(
    77  				`(?s)[/:a-zA-Z0-9$_=()"\.\|\-\\ ]*gtm(.exe"|)\s+status\s+--auto-log=gitlab\s+>>\s+\$1\.*`),
    78  		},
    79  	}
    80  
    81  	JiraHooks = map[string]scm.GitHook{
    82  		"prepare-commit-msg": {
    83  			Exe:     "gtm",
    84  			Command: "gtm status --auto-log=jira >> $1",
    85  			RE: regexp.MustCompile(
    86  				`(?s)[/:a-zA-Z0-9$_=()"\.\|\-\\ ]*gtm(.exe"|)\s+status\s+--auto-log=jira\s+>>\s+\$1\.*`),
    87  		},
    88  	}
    89  )
    90  
    91  const (
    92  	// NoteNameSpace is the gtm git note namespace
    93  	NoteNameSpace = "gtm-data"
    94  	// GTMDir is the subdir for gtm within the git repo root directory
    95  	GTMDir = ".gtm"
    96  )
    97  
    98  const initMsgTpl string = `
    99  {{print "Git Time Metric initialized for " (.ProjectPath) | printf (.HeaderFormat) }}
   100  
   101  {{ range $hook, $command := .GitHooks -}}
   102  	{{- $hook | printf "%20s" }}: {{ $command.Command }}
   103  {{ end -}}
   104  {{ range $key, $val := .GitConfig -}}
   105  	{{- $key | printf "%20s" }}: {{ $val }}
   106  {{end -}}
   107  {{ range $ref := .GitFetchRefs -}}
   108      {{ print "add fetch ref:" | printf "%21s" }} {{ $ref}}
   109  {{end -}}
   110  {{ print "terminal:" | printf "%21s" }} {{ .Terminal }}
   111  {{ print ".gitignore:" | printf "%21s" }} {{ .GitIgnore }}
   112  {{ print "tags:" | printf "%21s" }} {{.Tags }}
   113  `
   114  const removeMsgTpl string = `
   115  {{print "Git Time Metric uninitialized for " (.ProjectPath) | printf (.HeaderFormat) }}
   116  
   117  The following items have been removed.
   118  
   119  {{ range $hook, $command := .GitHooks -}}
   120  	{{- $hook | printf "%20s" }}: {{ $command.Command }}
   121  {{ end -}}
   122  {{ range $key, $val := .GitConfig -}}
   123  	{{- $key | printf "%20s" }}: {{ $val }}
   124  {{end -}}
   125  {{ print ".gitignore:" | printf "%21s" }} {{ .GitIgnore }}
   126  `
   127  
   128  // Initialize initializes a git repo for time tracking
   129  func Initialize(
   130  	terminal bool,
   131  	tags []string,
   132  	clearTags bool,
   133  	autoLog string,
   134  	local bool,
   135  	cwd string,
   136  ) (string, error) {
   137  	var (
   138  		wd  string
   139  		err error
   140  	)
   141  	gitRepoPath, workDirRoot, gtmPath, err := SetUpPaths(cwd, wd, err)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  
   146  	if clearTags {
   147  		err = removeTags(gtmPath)
   148  		if err != nil {
   149  			return "", err
   150  		}
   151  	}
   152  	tags, err = SetupTags(err, tags, gtmPath)
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  
   157  	if terminal {
   158  		if err := ioutil.WriteFile(filepath.Join(gtmPath, "terminal.app"), []byte(""), 0644); err != nil {
   159  			return "", err
   160  		}
   161  	} else {
   162  		// try to remove terminal.app, it may not exist
   163  		_ = os.Remove(filepath.Join(gtmPath, "terminal.app"))
   164  	}
   165  
   166  	err = SetupHooks(local, gitRepoPath, autoLog)
   167  	if err != nil {
   168  		return "", err
   169  	}
   170  
   171  	if err := scm.ConfigSet(GitConfig, gitRepoPath); err != nil {
   172  		return "", err
   173  	}
   174  
   175  	if err := scm.IgnoreSet(GitIgnore, workDirRoot); err != nil {
   176  		return "", err
   177  	}
   178  
   179  	headerFormat := "%s"
   180  	if isatty.IsTerminal(os.Stdout.Fd()) && runtime.GOOS != "windows" {
   181  		headerFormat = "\x1b[1m%s\x1b[0m"
   182  	}
   183  
   184  	b := new(bytes.Buffer)
   185  	t := template.Must(template.New("msg").Parse(initMsgTpl))
   186  	err = t.Execute(b,
   187  		struct {
   188  			Tags         string
   189  			HeaderFormat string
   190  			ProjectPath  string
   191  			GitHooks     map[string]scm.GitHook
   192  			GitConfig    map[string]string
   193  			GitFetchRefs []string
   194  			GitIgnore    string
   195  			Terminal     bool
   196  		}{
   197  			strings.Join(tags, " "),
   198  			headerFormat,
   199  			workDirRoot,
   200  			GitHooks,
   201  			GitConfig,
   202  			GitFetchRefs,
   203  			GitIgnore,
   204  			terminal,
   205  		})
   206  
   207  	if err != nil {
   208  		return "", err
   209  	}
   210  
   211  	index, err := NewIndex()
   212  	if err != nil {
   213  		return "", err
   214  	}
   215  
   216  	index.add(workDirRoot)
   217  	err = index.save()
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  
   222  	return b.String(), nil
   223  }
   224  
   225  func SetupHooks(local bool, gitRepoPath, autoLog string) error {
   226  	if !local {
   227  		if err := scm.FetchRemotesAddRefSpecs(GitFetchRefs, gitRepoPath); err != nil {
   228  			return err
   229  		}
   230  		for k, v := range GitPushRefsHooks {
   231  			GitHooks[k] = v
   232  		}
   233  	}
   234  
   235  	switch autoLog {
   236  	case "gitlab":
   237  		for k, v := range GitLabHooks {
   238  			GitHooks[k] = v
   239  		}
   240  	case "jira":
   241  		for k, v := range JiraHooks {
   242  			GitHooks[k] = v
   243  		}
   244  	}
   245  
   246  	if err := scm.SetHooks(GitHooks, gitRepoPath); err != nil {
   247  		return err
   248  	}
   249  	return nil
   250  }
   251  
   252  func SetupTags(err error, tags []string, gtmPath string) ([]string, error) {
   253  	err = saveTags(tags, gtmPath)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	tags, err = LoadTags(gtmPath)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	return tags, nil
   262  }
   263  
   264  func SetUpPaths(cwd, wd string, err error) (
   265  	gitRepoPath, workDirRoot, gtmPath string, setupError error) {
   266  	if cwd == "" {
   267  		wd, err = os.Getwd()
   268  	} else {
   269  		wd = cwd
   270  	}
   271  
   272  	if err != nil {
   273  		return "", "", "", err
   274  	}
   275  
   276  	gitRepoPath, err = scm.GitRepoPath(wd)
   277  	if err != nil {
   278  		return "", "", "", fmt.Errorf(
   279  			"Unable to initialize Git Time Metric, Git repository not found in '%s'", wd)
   280  	}
   281  	if _, err := os.Stat(gitRepoPath); os.IsNotExist(err) {
   282  		return "", "", "", fmt.Errorf(
   283  			"Unable to initialize Git Time Metric, Git repository not found in %s", gitRepoPath)
   284  	}
   285  
   286  	workDirRoot, err = scm.Workdir(gitRepoPath)
   287  	if err != nil {
   288  		return "", "", "", fmt.Errorf(
   289  			"Unable to initialize Git Time Metric, Git working tree root not found in %s", workDirRoot)
   290  
   291  	}
   292  
   293  	if _, err := os.Stat(workDirRoot); os.IsNotExist(err) {
   294  		return "", "", "", fmt.Errorf(
   295  			"Unable to initialize Git Time Metric, Git working tree root not found in %s", workDirRoot)
   296  	}
   297  
   298  	gtmPath = filepath.Join(workDirRoot, GTMDir)
   299  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   300  		if err := os.MkdirAll(gtmPath, 0700); err != nil {
   301  			return "", "", "", err
   302  		}
   303  	}
   304  	return gitRepoPath, workDirRoot, gtmPath, nil
   305  }
   306  
   307  //Uninitialize remove GTM tracking from the project in the current working directory
   308  func Uninitialize() (string, error) {
   309  	wd, err := os.Getwd()
   310  	if err != nil {
   311  		return "", err
   312  	}
   313  
   314  	gitRepoPath, err := scm.GitRepoPath(wd)
   315  	if err != nil {
   316  		return "", fmt.Errorf(
   317  			"Unable to uninitialize Git Time Metric, Git repository not found in %s", gitRepoPath)
   318  	}
   319  
   320  	workDir, _ := scm.Workdir(gitRepoPath)
   321  	gtmPath := filepath.Join(workDir, GTMDir)
   322  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   323  		return "", fmt.Errorf(
   324  			"Unable to uninitialize Git Time Metric, %s directory not found", gtmPath)
   325  	}
   326  	if err := scm.RemoveHooks(GitLabHooks, gitRepoPath); err != nil {
   327  		return "", err
   328  	}
   329  	if err := scm.RemoveHooks(GitHooks, gitRepoPath); err != nil {
   330  		return "", err
   331  	}
   332  	if err := scm.RemoveHooks(GitPushRefsHooks, gitRepoPath); err != nil {
   333  		return "", err
   334  	}
   335  	if err := scm.ConfigRemove(GitConfig, gitRepoPath); err != nil {
   336  		return "", err
   337  	}
   338  	if err := scm.FetchRemotesRemoveRefSpecs(GitFetchRefs, gitRepoPath); err != nil {
   339  		return "", err
   340  	}
   341  	if err := scm.IgnoreRemove(GitIgnore, workDir); err != nil {
   342  		return "", err
   343  	}
   344  	if err := os.RemoveAll(gtmPath); err != nil {
   345  		return "", err
   346  	}
   347  
   348  	headerFormat := "%s"
   349  	if isatty.IsTerminal(os.Stdout.Fd()) && runtime.GOOS != "windows" {
   350  		headerFormat = "\x1b[1m%s\x1b[0m"
   351  	}
   352  	b := new(bytes.Buffer)
   353  	t := template.Must(template.New("msg").Parse(removeMsgTpl))
   354  	err = t.Execute(b,
   355  		struct {
   356  			HeaderFormat string
   357  			ProjectPath  string
   358  			GitHooks     map[string]scm.GitHook
   359  			GitConfig    map[string]string
   360  			GitIgnore    string
   361  		}{
   362  			headerFormat,
   363  			workDir,
   364  			GitHooks,
   365  			GitConfig,
   366  			GitIgnore})
   367  
   368  	if err != nil {
   369  		return "", err
   370  	}
   371  
   372  	index, err := NewIndex()
   373  	if err != nil {
   374  		return "", err
   375  	}
   376  
   377  	index.remove(workDir)
   378  	err = index.save()
   379  	if err != nil {
   380  		return "", err
   381  	}
   382  
   383  	return b.String(), nil
   384  }
   385  
   386  //Clean removes any event or metrics files from project in the current working directory
   387  func Clean(dr util.DateRange, terminalOnly bool, appOnly bool) error {
   388  	wd, err := os.Getwd()
   389  	if err != nil {
   390  		return err
   391  	}
   392  
   393  	gitRepoPath, err := scm.GitRepoPath(wd)
   394  	if err != nil {
   395  		return fmt.Errorf("Unable to clean, Git repository not found in %s", gitRepoPath)
   396  	}
   397  
   398  	workDir, err := scm.Workdir(gitRepoPath)
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	gtmPath := filepath.Join(workDir, GTMDir)
   404  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   405  		return fmt.Errorf("Unable to clean GTM data, %s directory not found", gtmPath)
   406  	}
   407  
   408  	files, err := ioutil.ReadDir(gtmPath)
   409  	if err != nil {
   410  		return err
   411  	}
   412  	for _, f := range files {
   413  		if !strings.HasSuffix(f.Name(), ".event") &&
   414  			!strings.HasSuffix(f.Name(), ".metric") {
   415  			continue
   416  		}
   417  		if !dr.Within(f.ModTime()) {
   418  			continue
   419  		}
   420  
   421  		fp := filepath.Join(gtmPath, f.Name())
   422  		if (terminalOnly || appOnly) && strings.HasSuffix(f.Name(), ".event") {
   423  			b, err := ioutil.ReadFile(fp)
   424  			if err != nil {
   425  				return err
   426  			}
   427  
   428  			if terminalOnly {
   429  				if !strings.Contains(string(b), "terminal.app") {
   430  					continue
   431  				}
   432  			} else if appOnly {
   433  				if !AppEventFileContentRegex.MatchString(string(b)) {
   434  					continue
   435  				}
   436  			}
   437  		}
   438  
   439  		if err := os.Remove(fp); err != nil {
   440  			return err
   441  		}
   442  	}
   443  	return nil
   444  }
   445  
   446  // Paths returns the root git repo and gtm paths
   447  func Paths(wd ...string) (string, string, error) {
   448  	defer util.Profile()()
   449  
   450  	var (
   451  		gitRepoPath string
   452  		err         error
   453  	)
   454  	if len(wd) > 0 {
   455  		gitRepoPath, err = scm.GitRepoPath(wd[0])
   456  	} else {
   457  		gitRepoPath, err = scm.GitRepoPath()
   458  	}
   459  	if err != nil {
   460  		return "", "", ErrNotInitialized
   461  	}
   462  
   463  	workDir, err := scm.Workdir(gitRepoPath)
   464  	if err != nil {
   465  		return "", "", ErrNotInitialized
   466  	}
   467  
   468  	gtmPath := filepath.Join(workDir, GTMDir)
   469  	if _, err := os.Stat(gtmPath); os.IsNotExist(err) {
   470  		return "", "", ErrNotInitialized
   471  	}
   472  	return workDir, gtmPath, nil
   473  }
   474  
   475  func removeTags(gtmPath string) error {
   476  	files, err := ioutil.ReadDir(gtmPath)
   477  	if err != nil {
   478  		return err
   479  	}
   480  	for i := range files {
   481  		if strings.HasSuffix(files[i].Name(), ".tag") {
   482  			tagFile := filepath.Join(gtmPath, files[i].Name())
   483  			if err := os.Remove(tagFile); err != nil {
   484  				return err
   485  			}
   486  		}
   487  	}
   488  	return nil
   489  }
   490  
   491  // LoadTags returns the tags for the project in the gtmPath directory
   492  func LoadTags(gtmPath string) ([]string, error) {
   493  	var tags []string
   494  	files, err := ioutil.ReadDir(gtmPath)
   495  	if err != nil {
   496  		return []string{}, err
   497  	}
   498  	for i := range files {
   499  		if strings.HasSuffix(files[i].Name(), ".tag") {
   500  			tags = append(tags, strings.TrimSuffix(files[i].Name(), filepath.Ext(files[i].Name())))
   501  		}
   502  	}
   503  	return tags, nil
   504  }
   505  
   506  func saveTags(tags []string, gtmPath string) error {
   507  	if len(tags) > 0 {
   508  		for _, t := range tags {
   509  			if strings.TrimSpace(t) == "" {
   510  				continue
   511  			}
   512  			if err := ioutil.WriteFile(
   513  				filepath.Join(gtmPath, fmt.Sprintf("%s.tag", t)), []byte(""), 0644); err != nil {
   514  				return err
   515  			}
   516  		}
   517  	}
   518  	return nil
   519  }