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 }