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  }