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 }