github.com/kilpkonn/gtm-enhanced@v1.3.5/metric/metric.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 metric 6 7 import ( 8 "crypto/sha1" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "sort" 14 "strconv" 15 "strings" 16 17 "github.com/git-time-metric/gtm/epoch" 18 "github.com/git-time-metric/gtm/note" 19 "github.com/git-time-metric/gtm/scm" 20 "github.com/git-time-metric/gtm/util" 21 ) 22 23 // getFileID returns the SHA1 checksum for filePath 24 func getFileID(filePath string) string { 25 return fmt.Sprintf("%x", sha1.Sum([]byte(filepath.ToSlash(filePath)))) 26 } 27 28 // allocateTime calculates access time for each file within an epoch window 29 func allocateTime(ep int64, metricMap map[string]FileMetric, eventMap map[string]int) error { 30 total := 0 31 for file := range eventMap { 32 total += eventMap[file] 33 } 34 35 lastFileID := "" 36 timeAllocated := 0 37 for file := range eventMap { 38 t := int(float64(eventMap[file]) / float64(total) * float64(epoch.WindowSize)) 39 fileID := getFileID(file) 40 41 var ( 42 fm FileMetric 43 ok bool 44 err error 45 ) 46 fm, ok = metricMap[fileID] 47 if !ok { 48 fm, err = newFileMetric(file, 0, true, map[int64]int{}) 49 if err != nil { 50 return err 51 } 52 } 53 fm.AddTimeSpent(ep, t) 54 55 //NOTE: Go has some gotchas when it comes to structs contained within maps 56 // a copy is returned and not the reference to the struct 57 // https://groups.google.com/forum/#!topic/golang-nuts/4_pabWnsMp0 58 // assigning the new & updated metricFile instance to the map 59 metricMap[fileID] = fm 60 61 timeAllocated += t 62 lastFileID = fileID 63 } 64 // let's make sure all of the EpochWindowSize seconds are allocated 65 // we put the remaining on the last file 66 if lastFileID != "" && timeAllocated < epoch.WindowSize { 67 fm := metricMap[lastFileID] 68 fm.AddTimeSpent(ep, epoch.WindowSize-timeAllocated) 69 metricMap[lastFileID] = fm 70 } 71 return nil 72 } 73 74 // FileMetric contains the source file and it's time metrics 75 type FileMetric struct { 76 Updated bool // Updated signifies if we need to save the metric file 77 SourceFile string 78 TimeSpent int 79 Timeline map[int64]int 80 } 81 82 // AddTimeSpent accumulates time spent for a source file 83 func (f *FileMetric) AddTimeSpent(ep int64, t int) { 84 f.Updated = true 85 f.TimeSpent += t 86 f.Timeline[ep] += t 87 } 88 89 // Downsample return timeline by hour 90 func (f *FileMetric) Downsample() { 91 byHour := map[int64]int{} 92 for ep, t := range f.Timeline { 93 byHour[ep/3600*3600] += t 94 } 95 f.Timeline = byHour 96 } 97 98 // SortEpochs returns sorted timeline epochs 99 func (f *FileMetric) SortEpochs() []int64 { 100 keys := []int64{} 101 for k := range f.Timeline { 102 keys = append(keys, k) 103 } 104 sort.Sort(util.ByInt64(keys)) 105 return keys 106 } 107 108 // FileMetricByTime is an array of FileMetrics 109 type FileMetricByTime []FileMetric 110 111 func (a FileMetricByTime) Len() int { return len(a) } 112 func (a FileMetricByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 113 func (a FileMetricByTime) Less(i, j int) bool { return a[i].TimeSpent < a[j].TimeSpent } 114 115 func newFileMetric(f string, t int, updated bool, timeline map[int64]int) (FileMetric, error) { 116 return FileMetric{SourceFile: f, TimeSpent: t, Updated: updated, Timeline: timeline}, nil 117 } 118 119 // marshalFileMetric converts FileMetric struct to a byte array 120 func marshalFileMetric(fm FileMetric) []byte { 121 s := fmt.Sprintf("%s:%d", fm.SourceFile, fm.TimeSpent) 122 for _, e := range fm.SortEpochs() { 123 s += fmt.Sprintf(",%d:%d", e, fm.Timeline[e]) 124 } 125 return []byte(s) 126 } 127 128 // unMarshalFileMetric converts a byte array to a FileMetric struct 129 func unMarshalFileMetric(b []byte, filePath string) (FileMetric, error) { 130 var ( 131 fileName string 132 totalTimeSpent int 133 err error 134 ) 135 136 timeline := map[int64]int{} 137 parts := strings.Split(string(b), ",") 138 139 for i := 0; i < len(parts); i++ { 140 subparts := strings.Split(parts[i], ":") 141 if len(subparts) != 2 { 142 return FileMetric{}, fmt.Errorf("Unable to parse metric file %s, invalid format", filePath) 143 } 144 if i == 0 { 145 fileName = subparts[0] 146 totalTimeSpent, err = strconv.Atoi(subparts[1]) 147 if err != nil { 148 return FileMetric{}, fmt.Errorf("Unable to parse metric file %s, invalid time, %s", filePath, err) 149 } 150 continue 151 } 152 ep, err := strconv.ParseInt(subparts[0], 10, 64) 153 if err != nil { 154 return FileMetric{}, fmt.Errorf("Unable to parse metric file %s, invalid epoch, %s", filePath, err) 155 } 156 timeSpent, err := strconv.Atoi(subparts[1]) 157 if err != nil { 158 return FileMetric{}, fmt.Errorf("Unable to parse metric file %s, invalid time, %s", filePath, err) 159 } 160 timeline[ep] += timeSpent 161 } 162 163 fm, err := newFileMetric(fileName, totalTimeSpent, false, timeline) 164 if err != nil { 165 return FileMetric{}, err 166 } 167 168 return fm, nil 169 } 170 171 // loadMetrics scans the gtmPath for metric files and loads them 172 func loadMetrics(gtmPath string) (map[string]FileMetric, error) { 173 files, err := ioutil.ReadDir(gtmPath) 174 if err != nil { 175 return nil, err 176 } 177 178 metrics := map[string]FileMetric{} 179 for _, file := range files { 180 181 if !strings.HasSuffix(file.Name(), ".metric") { 182 continue 183 } 184 185 metricFilePath := filepath.Join(gtmPath, file.Name()) 186 187 metricFile, err := readMetricFile(metricFilePath) 188 if err != nil { 189 // assume it's bad, remove it 190 _ = os.Remove(metricFilePath) 191 continue 192 } 193 194 metrics[strings.Replace(file.Name(), ".metric", "", 1)] = metricFile 195 } 196 197 return metrics, nil 198 } 199 200 // saveAndPurgeMetrics deletes metric files that are in the commit and save any that are not 201 func saveAndPurgeMetrics( 202 gtmPath string, 203 metricMap map[string]FileMetric, 204 commitMap map[string]FileMetric, 205 readonlyMap map[string]FileMetric) error { 206 207 for fileID, fm := range metricMap { 208 _, inCommitMap := commitMap[fileID] 209 _, inReadonlyMap := readonlyMap[fileID] 210 211 //Save metric files that are updated and not in commit or readonly maps 212 if fm.Updated && !inCommitMap && !inReadonlyMap { 213 if err := writeMetricFile(gtmPath, fm); err != nil { 214 return err 215 } 216 } 217 218 //Purge metric files that are in the commit and readonly maps 219 if inCommitMap || inReadonlyMap { 220 if err := removeMetricFile(gtmPath, fileID); err != nil { 221 return err 222 } 223 } 224 } 225 return nil 226 } 227 228 // readMetric reads and returns the unmarshalled metric file 229 func readMetricFile(filePath string) (FileMetric, error) { 230 b, err := ioutil.ReadFile(filePath) 231 if err != nil { 232 return FileMetric{}, err 233 } 234 235 return unMarshalFileMetric(b, filePath) 236 } 237 238 // writeMetricFile persists metric file to disk 239 func writeMetricFile(gtmPath string, fm FileMetric) error { 240 return ioutil.WriteFile( 241 filepath.Join(gtmPath, fmt.Sprintf("%s.metric", getFileID(fm.SourceFile))), 242 marshalFileMetric(fm), 0644) 243 } 244 245 // removeMetricFile deletes a metric file with fileID 246 func removeMetricFile(gtmPath, fileID string) error { 247 fp := filepath.Join(gtmPath, fmt.Sprintf("%s.metric", fileID)) 248 if _, err := os.Stat(fp); os.IsNotExist(err) { 249 return nil 250 } 251 return os.Remove(fp) 252 } 253 254 // buildCommitMaps creates the write and read-only commit maps. 255 // Files that are in the head commit are added to write commit map. 256 // Files that are are not in the commit map and are readonly are added to the read-only commit map. 257 func buildCommitMaps(metricMap map[string]FileMetric) (map[string]FileMetric, map[string]FileMetric, error) { 258 commitMap := map[string]FileMetric{} 259 readonlyMap := map[string]FileMetric{} 260 261 commit, err := scm.HeadCommit() 262 if err != nil { 263 return commitMap, readonlyMap, err 264 } 265 266 for _, f := range commit.Stats.Files { 267 fileID := getFileID(f) 268 if _, ok := metricMap[fileID]; !ok { 269 continue 270 } 271 commitMap[fileID] = metricMap[fileID] 272 } 273 274 for fileID, fm := range metricMap { 275 // Look at files not in commit map 276 if _, ok := commitMap[fileID]; !ok { 277 status, err := scm.NewStatus() 278 if err != nil { 279 return commitMap, readonlyMap, err 280 } 281 282 if !status.IsModified(fm.SourceFile, false) { 283 readonlyMap[fileID] = fm 284 } 285 } 286 } 287 288 return commitMap, readonlyMap, nil 289 } 290 291 // buildCommitNote creates a CommitNote for files in the commit and readonly maps in git repo at rootPath 292 func buildCommitNote( 293 rootPath string, 294 commitMap map[string]FileMetric, 295 readonlyMap map[string]FileMetric) (note.CommitNote, error) { 296 297 defer util.Profile()() 298 299 flsModified := []note.FileDetail{} 300 301 for _, fm := range commitMap { 302 fm.Downsample() 303 status := "m" 304 if _, err := os.Stat(filepath.Join(rootPath, fm.SourceFile)); os.IsNotExist(err) { 305 status = "d" 306 } 307 flsModified = append( 308 flsModified, 309 note.FileDetail{SourceFile: fm.SourceFile, TimeSpent: fm.TimeSpent, Timeline: fm.Timeline, Status: status}) 310 } 311 312 flsReadonly := []note.FileDetail{} 313 for _, fm := range readonlyMap { 314 fm.Downsample() 315 status := "r" 316 if _, err := os.Stat(filepath.Join(rootPath, fm.SourceFile)); os.IsNotExist(err) { 317 status = "d" 318 } 319 flsReadonly = append( 320 flsReadonly, 321 note.FileDetail{SourceFile: fm.SourceFile, TimeSpent: fm.TimeSpent, Timeline: fm.Timeline, Status: status}) 322 } 323 fls := append(flsModified, flsReadonly...) 324 sort.Sort(sort.Reverse(note.FileByTime(fls))) 325 326 return note.CommitNote{Files: fls}, nil 327 } 328 329 // buildInterimCommitMaps creates the write and read-only commit maps 330 // Write and read-only files maps are built based on an algorithm and not a git commit 331 func buildInterimCommitMaps(metricMap map[string]FileMetric, projPath ...string) (map[string]FileMetric, map[string]FileMetric, error) { 332 defer util.Profile()() 333 334 commitMap := map[string]FileMetric{} 335 readonlyMap := map[string]FileMetric{} 336 337 status, err := scm.NewStatus(projPath...) 338 if err != nil { 339 return commitMap, readonlyMap, err 340 } 341 342 for fileID, fm := range metricMap { 343 if status.HasStaged() { 344 if status.IsModified(fm.SourceFile, true) { 345 commitMap[fileID] = fm 346 } else { 347 // when in staging, include any files in working that are not modified 348 if !status.IsModified(fm.SourceFile, false) { 349 readonlyMap[fileID] = fm 350 } 351 } 352 } else { 353 if status.IsModified(fm.SourceFile, false) { 354 commitMap[fileID] = fm 355 } else { 356 readonlyMap[fileID] = fm 357 } 358 } 359 } 360 361 return commitMap, readonlyMap, nil 362 }