|
4 | 4 | "bytes"
|
5 | 5 | "fmt"
|
6 | 6 | "io"
|
| 7 | + "math" |
| 8 | + "strings" |
7 | 9 |
|
8 | 10 | "gopkg.in/src-d/go-git.v4/plumbing"
|
9 | 11 | "gopkg.in/src-d/go-git.v4/plumbing/filemode"
|
@@ -105,6 +107,10 @@ func (p *Patch) Encode(w io.Writer) error {
|
105 | 107 | return ue.Encode(p)
|
106 | 108 | }
|
107 | 109 |
|
| 110 | +func (p *Patch) Stats() FileStats { |
| 111 | + return getFileStatsFromFilePatches(p.FilePatches()) |
| 112 | +} |
| 113 | + |
108 | 114 | func (p *Patch) String() string {
|
109 | 115 | buf := bytes.NewBuffer(nil)
|
110 | 116 | err := p.Encode(buf)
|
@@ -185,3 +191,112 @@ func (t *textChunk) Content() string {
|
185 | 191 | func (t *textChunk) Type() fdiff.Operation {
|
186 | 192 | return t.op
|
187 | 193 | }
|
| 194 | + |
| 195 | +// FileStat stores the status of changes in content of a file. |
| 196 | +type FileStat struct { |
| 197 | + Name string |
| 198 | + Addition int |
| 199 | + Deletion int |
| 200 | +} |
| 201 | + |
| 202 | +func (fs FileStat) String() string { |
| 203 | + return printStat([]FileStat{fs}) |
| 204 | +} |
| 205 | + |
| 206 | +// FileStats is a collection of FileStat. |
| 207 | +type FileStats []FileStat |
| 208 | + |
| 209 | +func (fileStats FileStats) String() string { |
| 210 | + return printStat(fileStats) |
| 211 | +} |
| 212 | + |
| 213 | +func printStat(fileStats []FileStat) string { |
| 214 | + padLength := float64(len(" ")) |
| 215 | + newlineLength := float64(len("\n")) |
| 216 | + separatorLength := float64(len("|")) |
| 217 | + // Soft line length limit. The text length calculation below excludes |
| 218 | + // length of the change number. Adding that would take it closer to 80, |
| 219 | + // but probably not more than 80, until it's a huge number. |
| 220 | + lineLength := 72.0 |
| 221 | + |
| 222 | + // Get the longest filename and longest total change. |
| 223 | + var longestLength float64 |
| 224 | + var longestTotalChange float64 |
| 225 | + for _, fs := range fileStats { |
| 226 | + if int(longestLength) < len(fs.Name) { |
| 227 | + longestLength = float64(len(fs.Name)) |
| 228 | + } |
| 229 | + totalChange := fs.Addition + fs.Deletion |
| 230 | + if int(longestTotalChange) < totalChange { |
| 231 | + longestTotalChange = float64(totalChange) |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + // Parts of the output: |
| 236 | + // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline> |
| 237 | + // example: " main.go | 10 +++++++--- " |
| 238 | + |
| 239 | + // <pad><filename><pad> |
| 240 | + leftTextLength := padLength + longestLength + padLength |
| 241 | + |
| 242 | + // <pad><number><pad><+++++/-----><newline> |
| 243 | + // Excluding number length here. |
| 244 | + rightTextLength := padLength + padLength + newlineLength |
| 245 | + |
| 246 | + totalTextArea := leftTextLength + separatorLength + rightTextLength |
| 247 | + heightOfHistogram := lineLength - totalTextArea |
| 248 | + |
| 249 | + // Scale the histogram. |
| 250 | + var scaleFactor float64 |
| 251 | + if longestTotalChange > heightOfHistogram { |
| 252 | + // Scale down to heightOfHistogram. |
| 253 | + scaleFactor = float64(longestTotalChange / heightOfHistogram) |
| 254 | + } else { |
| 255 | + scaleFactor = 1.0 |
| 256 | + } |
| 257 | + |
| 258 | + finalOutput := "" |
| 259 | + for _, fs := range fileStats { |
| 260 | + addn := float64(fs.Addition) |
| 261 | + deln := float64(fs.Deletion) |
| 262 | + adds := strings.Repeat("+", int(math.Floor(addn/scaleFactor))) |
| 263 | + dels := strings.Repeat("-", int(math.Floor(deln/scaleFactor))) |
| 264 | + finalOutput += fmt.Sprintf(" %s | %d %s%s\n", fs.Name, (fs.Addition + fs.Deletion), adds, dels) |
| 265 | + } |
| 266 | + |
| 267 | + return finalOutput |
| 268 | +} |
| 269 | + |
| 270 | +func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats { |
| 271 | + var fileStats FileStats |
| 272 | + |
| 273 | + for _, fp := range filePatches { |
| 274 | + cs := FileStat{} |
| 275 | + from, to := fp.Files() |
| 276 | + if from == nil { |
| 277 | + // New File is created. |
| 278 | + cs.Name = to.Path() |
| 279 | + } else if to == nil { |
| 280 | + // File is deleted. |
| 281 | + cs.Name = from.Path() |
| 282 | + } else if from.Path() != to.Path() { |
| 283 | + // File is renamed. Not supported. |
| 284 | + // cs.Name = fmt.Sprintf("%s => %s", from.Path(), to.Path()) |
| 285 | + } else { |
| 286 | + cs.Name = from.Path() |
| 287 | + } |
| 288 | + |
| 289 | + for _, chunk := range fp.Chunks() { |
| 290 | + switch chunk.Type() { |
| 291 | + case fdiff.Add: |
| 292 | + cs.Addition += strings.Count(chunk.Content(), "\n") |
| 293 | + case fdiff.Delete: |
| 294 | + cs.Deletion += strings.Count(chunk.Content(), "\n") |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + fileStats = append(fileStats, cs) |
| 299 | + } |
| 300 | + |
| 301 | + return fileStats |
| 302 | +} |
0 commit comments