|  | @@ -19,7 +19,8 @@ import (
 | 
	
		
			
				|  |  |  	"gopkg.in/src-d/hercules.v3/yaml"
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -// BurndownAnalyser allows to gather the line burndown statistics for a Git repository.
 | 
	
		
			
				|  |  | +// BurndownAnalysis allows to gather the line burndown statistics for a Git repository.
 | 
	
		
			
				|  |  | +// It is a LeafPipelineItem.
 | 
	
		
			
				|  |  |  // Reference: https://erikbern.com/2016/12/05/the-half-life-of-code.html
 | 
	
		
			
				|  |  |  type BurndownAnalysis struct {
 | 
	
		
			
				|  |  |  	// Granularity sets the size of each band - the number of days it spans.
 | 
	
	
		
			
				|  | @@ -68,7 +69,8 @@ type BurndownAnalysis struct {
 | 
	
		
			
				|  |  |  	reversedPeopleDict []string
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -// Carries the result of running BurndownAnalysis - it is returned by BurndownAnalysis.Finalize().
 | 
	
		
			
				|  |  | +// BurndownResult carries the result of running BurndownAnalysis - it is returned by
 | 
	
		
			
				|  |  | +// BurndownAnalysis.Finalize().
 | 
	
		
			
				|  |  |  type BurndownResult struct {
 | 
	
		
			
				|  |  |  	// [number of samples][number of bands]
 | 
	
		
			
				|  |  |  	// The number of samples depends on Sampling: the less Sampling, the bigger the number.
 | 
	
	
		
			
				|  | @@ -99,15 +101,22 @@ type BurndownResult struct {
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const (
 | 
	
		
			
				|  |  | +	// ConfigBurndownGranularity is the name of the option to set BurndownAnalysis.Granularity.
 | 
	
		
			
				|  |  |  	ConfigBurndownGranularity = "Burndown.Granularity"
 | 
	
		
			
				|  |  | +	// ConfigBurndownSampling is the name of the option to set BurndownAnalysis.Sampling.
 | 
	
		
			
				|  |  |  	ConfigBurndownSampling    = "Burndown.Sampling"
 | 
	
		
			
				|  |  | -	// Measuring individual files is optional and false by default.
 | 
	
		
			
				|  |  | +	// ConfigBurndownTrackFiles enables burndown collection for files.
 | 
	
		
			
				|  |  |  	ConfigBurndownTrackFiles = "Burndown.TrackFiles"
 | 
	
		
			
				|  |  | -	// Measuring authors is optional and false by default.
 | 
	
		
			
				|  |  | +	// ConfigBurndownTrackPeople enables burndown collection for authors.
 | 
	
		
			
				|  |  |  	ConfigBurndownTrackPeople = "Burndown.TrackPeople"
 | 
	
		
			
				|  |  | -	// Enables some extra debug assertions.
 | 
	
		
			
				|  |  | +	// ConfigBurndownDebug enables some extra debug assertions.
 | 
	
		
			
				|  |  |  	ConfigBurndownDebug        = "Burndown.Debug"
 | 
	
		
			
				|  |  | +	// DefaultBurndownGranularity is the default number of days for BurndownAnalysis.Granularity
 | 
	
		
			
				|  |  | +	// and BurndownAnalysis.Sampling.
 | 
	
		
			
				|  |  |  	DefaultBurndownGranularity = 30
 | 
	
		
			
				|  |  | +	// authorSelf is the internal author index which is used in BurndownAnalysis.Finalize() to
 | 
	
		
			
				|  |  | +	// format the author overwrites matrix.
 | 
	
		
			
				|  |  | +	authorSelf = (1 << 18) - 2
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) Name() string {
 | 
	
	
		
			
				|  | @@ -263,9 +272,9 @@ func (analyser *BurndownAnalysis) Finalize() interface{} {
 | 
	
		
			
				|  |  |  		mrow := make([]int64, analyser.PeopleNumber+2)
 | 
	
		
			
				|  |  |  		peopleMatrix[i] = mrow
 | 
	
		
			
				|  |  |  		for key, val := range row {
 | 
	
		
			
				|  |  | -			if key == MISSING_AUTHOR {
 | 
	
		
			
				|  |  | +			if key == AuthorMissing {
 | 
	
		
			
				|  |  |  				key = -1
 | 
	
		
			
				|  |  | -			} else if key == SELF_AUTHOR {
 | 
	
		
			
				|  |  | +			} else if key == authorSelf {
 | 
	
		
			
				|  |  |  				key = -2
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  			mrow[key+2] = val
 | 
	
	
		
			
				|  | @@ -772,55 +781,56 @@ func (analyser *BurndownAnalysis) packPersonWithDay(person int, day int) int {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) unpackPersonWithDay(value int) (int, int) {
 | 
	
		
			
				|  |  |  	if analyser.PeopleNumber == 0 {
 | 
	
		
			
				|  |  | -		return MISSING_AUTHOR, value
 | 
	
		
			
				|  |  | +		return AuthorMissing, value
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	return value >> 14, value & 0x3FFF
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) updateStatus(
 | 
	
		
			
				|  |  | -	status interface{}, _ int, previous_time_ int, delta int) {
 | 
	
		
			
				|  |  | +	status interface{}, _ int, previousValue int, delta int) {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	_, previous_time := analyser.unpackPersonWithDay(previous_time_)
 | 
	
		
			
				|  |  | -	status.(map[int]int64)[previous_time] += int64(delta)
 | 
	
		
			
				|  |  | +	_, previousTime := analyser.unpackPersonWithDay(previousValue)
 | 
	
		
			
				|  |  | +	status.(map[int]int64)[previousTime] += int64(delta)
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func (analyser *BurndownAnalysis) updatePeople(people interface{}, _ int, previous_time_ int, delta int) {
 | 
	
		
			
				|  |  | -	old_author, previous_time := analyser.unpackPersonWithDay(previous_time_)
 | 
	
		
			
				|  |  | -	if old_author == MISSING_AUTHOR {
 | 
	
		
			
				|  |  | +func (analyser *BurndownAnalysis) updatePeople(
 | 
	
		
			
				|  |  | +	peopleUncasted interface{}, _ int, previousValue int, delta int) {
 | 
	
		
			
				|  |  | +	previousAuthor, previousTime := analyser.unpackPersonWithDay(previousValue)
 | 
	
		
			
				|  |  | +	if previousAuthor == AuthorMissing {
 | 
	
		
			
				|  |  |  		return
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	casted := people.([]map[int]int64)
 | 
	
		
			
				|  |  | -	stats := casted[old_author]
 | 
	
		
			
				|  |  | +	people := peopleUncasted.([]map[int]int64)
 | 
	
		
			
				|  |  | +	stats := people[previousAuthor]
 | 
	
		
			
				|  |  |  	if stats == nil {
 | 
	
		
			
				|  |  |  		stats = map[int]int64{}
 | 
	
		
			
				|  |  | -		casted[old_author] = stats
 | 
	
		
			
				|  |  | +		people[previousAuthor] = stats
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	stats[previous_time] += int64(delta)
 | 
	
		
			
				|  |  | +	stats[previousTime] += int64(delta)
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) updateMatrix(
 | 
	
		
			
				|  |  | -	matrix_ interface{}, current_time int, previous_time int, delta int) {
 | 
	
		
			
				|  |  | +	matrixUncasted interface{}, currentTime int, previousTime int, delta int) {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	matrix := matrix_.([]map[int]int64)
 | 
	
		
			
				|  |  | -	new_author, _ := analyser.unpackPersonWithDay(current_time)
 | 
	
		
			
				|  |  | -	old_author, _ := analyser.unpackPersonWithDay(previous_time)
 | 
	
		
			
				|  |  | -	if old_author == MISSING_AUTHOR {
 | 
	
		
			
				|  |  | +	matrix := matrixUncasted.([]map[int]int64)
 | 
	
		
			
				|  |  | +	newAuthor, _ := analyser.unpackPersonWithDay(currentTime)
 | 
	
		
			
				|  |  | +	oldAuthor, _ := analyser.unpackPersonWithDay(previousTime)
 | 
	
		
			
				|  |  | +	if oldAuthor == AuthorMissing {
 | 
	
		
			
				|  |  |  		return
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	if new_author == old_author && delta > 0 {
 | 
	
		
			
				|  |  | -		new_author = SELF_AUTHOR
 | 
	
		
			
				|  |  | +	if newAuthor == oldAuthor && delta > 0 {
 | 
	
		
			
				|  |  | +		newAuthor = authorSelf
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	row := matrix[old_author]
 | 
	
		
			
				|  |  | +	row := matrix[oldAuthor]
 | 
	
		
			
				|  |  |  	if row == nil {
 | 
	
		
			
				|  |  |  		row = map[int]int64{}
 | 
	
		
			
				|  |  | -		matrix[old_author] = row
 | 
	
		
			
				|  |  | +		matrix[oldAuthor] = row
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	cell, exists := row[new_author]
 | 
	
		
			
				|  |  | +	cell, exists := row[newAuthor]
 | 
	
		
			
				|  |  |  	if !exists {
 | 
	
		
			
				|  |  | -		row[new_author] = 0
 | 
	
		
			
				|  |  | +		row[newAuthor] = 0
 | 
	
		
			
				|  |  |  		cell = 0
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	row[new_author] = cell + int64(delta)
 | 
	
		
			
				|  |  | +	row[newAuthor] = cell + int64(delta)
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) newFile(
 | 
	
	
		
			
				|  | @@ -852,7 +862,7 @@ func (analyser *BurndownAnalysis) handleInsertion(
 | 
	
		
			
				|  |  |  	name := change.To.Name
 | 
	
		
			
				|  |  |  	file, exists := analyser.files[name]
 | 
	
		
			
				|  |  |  	if exists {
 | 
	
		
			
				|  |  | -		return errors.New(fmt.Sprintf("file %s already exists", name))
 | 
	
		
			
				|  |  | +		return fmt.Errorf("file %s already exists", name)
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	file = analyser.newFile(
 | 
	
		
			
				|  |  |  		author, analyser.day, lines, analyser.globalStatus, analyser.people, analyser.matrix)
 | 
	
	
		
			
				|  | @@ -899,9 +909,9 @@ func (analyser *BurndownAnalysis) handleModification(
 | 
	
		
			
				|  |  |  	thisDiffs := diffs[change.To.Name]
 | 
	
		
			
				|  |  |  	if file.Len() != thisDiffs.OldLinesOfCode {
 | 
	
		
			
				|  |  |  		fmt.Fprintf(os.Stderr, "====TREE====\n%s", file.Dump())
 | 
	
		
			
				|  |  | -		return errors.New(fmt.Sprintf("%s: internal integrity error src %d != %d %s -> %s",
 | 
	
		
			
				|  |  | +		return fmt.Errorf("%s: internal integrity error src %d != %d %s -> %s",
 | 
	
		
			
				|  |  |  			change.To.Name, thisDiffs.OldLinesOfCode, file.Len(),
 | 
	
		
			
				|  |  | -			change.From.TreeEntry.Hash.String(), change.To.TreeEntry.Hash.String()))
 | 
	
		
			
				|  |  | +			change.From.TreeEntry.Hash.String(), change.To.TreeEntry.Hash.String())
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	// we do not call RunesToDiffLines so the number of lines equals
 | 
	
	
		
			
				|  | @@ -923,17 +933,17 @@ func (analyser *BurndownAnalysis) handleModification(
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	for _, edit := range thisDiffs.Diffs {
 | 
	
		
			
				|  |  | -		dump_before := ""
 | 
	
		
			
				|  |  | +		dumpBefore := ""
 | 
	
		
			
				|  |  |  		if analyser.Debug {
 | 
	
		
			
				|  |  | -			dump_before = file.Dump()
 | 
	
		
			
				|  |  | +			dumpBefore = file.Dump()
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  		length := utf8.RuneCountInString(edit.Text)
 | 
	
		
			
				|  |  | -		debug_error := func() {
 | 
	
		
			
				|  |  | +		debugError := func() {
 | 
	
		
			
				|  |  |  			fmt.Fprintf(os.Stderr, "%s: internal diff error\n", change.To.Name)
 | 
	
		
			
				|  |  |  			fmt.Fprintf(os.Stderr, "Update(%d, %d, %d (0), %d (0))\n", analyser.day, position,
 | 
	
		
			
				|  |  |  				length, utf8.RuneCountInString(pending.Text))
 | 
	
		
			
				|  |  | -			if dump_before != "" {
 | 
	
		
			
				|  |  | -				fmt.Fprintf(os.Stderr, "====TREE BEFORE====\n%s====END====\n", dump_before)
 | 
	
		
			
				|  |  | +			if dumpBefore != "" {
 | 
	
		
			
				|  |  | +				fmt.Fprintf(os.Stderr, "====TREE BEFORE====\n%s====END====\n", dumpBefore)
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  			fmt.Fprintf(os.Stderr, "====TREE AFTER====\n%s====END====\n", file.Dump())
 | 
	
		
			
				|  |  |  		}
 | 
	
	
		
			
				|  | @@ -947,7 +957,7 @@ func (analyser *BurndownAnalysis) handleModification(
 | 
	
		
			
				|  |  |  		case diffmatchpatch.DiffInsert:
 | 
	
		
			
				|  |  |  			if pending.Text != "" {
 | 
	
		
			
				|  |  |  				if pending.Type == diffmatchpatch.DiffInsert {
 | 
	
		
			
				|  |  | -					debug_error()
 | 
	
		
			
				|  |  | +					debugError()
 | 
	
		
			
				|  |  |  					return errors.New("DiffInsert may not appear after DiffInsert")
 | 
	
		
			
				|  |  |  				}
 | 
	
		
			
				|  |  |  				file.Update(analyser.packPersonWithDay(author, analyser.day), position, length,
 | 
	
	
		
			
				|  | @@ -962,13 +972,13 @@ func (analyser *BurndownAnalysis) handleModification(
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  		case diffmatchpatch.DiffDelete:
 | 
	
		
			
				|  |  |  			if pending.Text != "" {
 | 
	
		
			
				|  |  | -				debug_error()
 | 
	
		
			
				|  |  | +				debugError()
 | 
	
		
			
				|  |  |  				return errors.New("DiffDelete may not appear after DiffInsert/DiffDelete")
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  			pending = edit
 | 
	
		
			
				|  |  |  		default:
 | 
	
		
			
				|  |  | -			debug_error()
 | 
	
		
			
				|  |  | -			return errors.New(fmt.Sprintf("diff operation is not supported: %d", edit.Type))
 | 
	
		
			
				|  |  | +			debugError()
 | 
	
		
			
				|  |  | +			return fmt.Errorf("diff operation is not supported: %d", edit.Type)
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	if pending.Text != "" {
 | 
	
	
		
			
				|  | @@ -976,8 +986,8 @@ func (analyser *BurndownAnalysis) handleModification(
 | 
	
		
			
				|  |  |  		pending.Text = ""
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	if file.Len() != thisDiffs.NewLinesOfCode {
 | 
	
		
			
				|  |  | -		return errors.New(fmt.Sprintf("%s: internal integrity error dst %d != %d",
 | 
	
		
			
				|  |  | -			change.To.Name, thisDiffs.NewLinesOfCode, file.Len()))
 | 
	
		
			
				|  |  | +		return fmt.Errorf("%s: internal integrity error dst %d != %d",
 | 
	
		
			
				|  |  | +			change.To.Name, thisDiffs.NewLinesOfCode, file.Len())
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	return nil
 | 
	
		
			
				|  |  |  }
 | 
	
	
		
			
				|  | @@ -985,7 +995,7 @@ func (analyser *BurndownAnalysis) handleModification(
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) handleRename(from, to string) error {
 | 
	
		
			
				|  |  |  	file, exists := analyser.files[from]
 | 
	
		
			
				|  |  |  	if !exists {
 | 
	
		
			
				|  |  | -		return errors.New(fmt.Sprintf("file %s does not exist", from))
 | 
	
		
			
				|  |  | +		return fmt.Errorf("file %s does not exist", from)
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	analyser.files[to] = file
 | 
	
		
			
				|  |  |  	delete(analyser.files, from)
 | 
	
	
		
			
				|  | @@ -1053,15 +1063,15 @@ func (analyser *BurndownAnalysis) groupStatus() ([]int64, map[string][]int64, []
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) updateHistories(
 | 
	
		
			
				|  |  | -	globalStatus []int64, file_statuses map[string][]int64, people_statuses [][]int64, delta int) {
 | 
	
		
			
				|  |  | +	globalStatus []int64, fileStatuses map[string][]int64, peopleStatuses [][]int64, delta int) {
 | 
	
		
			
				|  |  |  	for i := 0; i < delta; i++ {
 | 
	
		
			
				|  |  |  		analyser.globalHistory = append(analyser.globalHistory, globalStatus)
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	to_delete := make([]string, 0)
 | 
	
		
			
				|  |  | +	toDelete := make([]string, 0)
 | 
	
		
			
				|  |  |  	for key, fh := range analyser.fileHistories {
 | 
	
		
			
				|  |  | -		ls, exists := file_statuses[key]
 | 
	
		
			
				|  |  | +		ls, exists := fileStatuses[key]
 | 
	
		
			
				|  |  |  		if !exists {
 | 
	
		
			
				|  |  | -			to_delete = append(to_delete, key)
 | 
	
		
			
				|  |  | +			toDelete = append(toDelete, key)
 | 
	
		
			
				|  |  |  		} else {
 | 
	
		
			
				|  |  |  			for i := 0; i < delta; i++ {
 | 
	
		
			
				|  |  |  				fh = append(fh, ls)
 | 
	
	
		
			
				|  | @@ -1069,10 +1079,10 @@ func (analyser *BurndownAnalysis) updateHistories(
 | 
	
		
			
				|  |  |  			analyser.fileHistories[key] = fh
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	for _, key := range to_delete {
 | 
	
		
			
				|  |  | +	for _, key := range toDelete {
 | 
	
		
			
				|  |  |  		delete(analyser.fileHistories, key)
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	for key, ls := range file_statuses {
 | 
	
		
			
				|  |  | +	for key, ls := range fileStatuses {
 | 
	
		
			
				|  |  |  		fh, exists := analyser.fileHistories[key]
 | 
	
		
			
				|  |  |  		if exists {
 | 
	
		
			
				|  |  |  			continue
 | 
	
	
		
			
				|  | @@ -1084,7 +1094,7 @@ func (analyser *BurndownAnalysis) updateHistories(
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	for key, ph := range analyser.peopleHistories {
 | 
	
		
			
				|  |  | -		ls := people_statuses[key]
 | 
	
		
			
				|  |  | +		ls := peopleStatuses[key]
 | 
	
		
			
				|  |  |  		for i := 0; i < delta; i++ {
 | 
	
		
			
				|  |  |  			ph = append(ph, ls)
 | 
	
		
			
				|  |  |  		}
 |