| package object |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| "math" |
| "strings" |
| |
| "gopkg.in/src-d/go-git.v4/plumbing" |
| "gopkg.in/src-d/go-git.v4/plumbing/filemode" |
| fdiff "gopkg.in/src-d/go-git.v4/plumbing/format/diff" |
| "gopkg.in/src-d/go-git.v4/utils/diff" |
| |
| dmp "github.com/sergi/go-diff/diffmatchpatch" |
| ) |
| |
| var ( |
| ErrCanceled = errors.New("operation canceled") |
| ) |
| |
| func getPatch(message string, changes ...*Change) (*Patch, error) { |
| ctx := context.Background() |
| return getPatchContext(ctx, message, changes...) |
| } |
| |
| func getPatchContext(ctx context.Context, message string, changes ...*Change) (*Patch, error) { |
| var filePatches []fdiff.FilePatch |
| for _, c := range changes { |
| select { |
| case <-ctx.Done(): |
| return nil, ErrCanceled |
| default: |
| } |
| |
| fp, err := filePatchWithContext(ctx, c) |
| if err != nil { |
| return nil, err |
| } |
| |
| filePatches = append(filePatches, fp) |
| } |
| |
| return &Patch{message, filePatches}, nil |
| } |
| |
| func filePatchWithContext(ctx context.Context, c *Change) (fdiff.FilePatch, error) { |
| from, to, err := c.Files() |
| if err != nil { |
| return nil, err |
| } |
| fromContent, fIsBinary, err := fileContent(from) |
| if err != nil { |
| return nil, err |
| } |
| |
| toContent, tIsBinary, err := fileContent(to) |
| if err != nil { |
| return nil, err |
| } |
| |
| if fIsBinary || tIsBinary { |
| return &textFilePatch{from: c.From, to: c.To}, nil |
| } |
| |
| diffs := diff.Do(fromContent, toContent) |
| |
| var chunks []fdiff.Chunk |
| for _, d := range diffs { |
| select { |
| case <-ctx.Done(): |
| return nil, ErrCanceled |
| default: |
| } |
| |
| var op fdiff.Operation |
| switch d.Type { |
| case dmp.DiffEqual: |
| op = fdiff.Equal |
| case dmp.DiffDelete: |
| op = fdiff.Delete |
| case dmp.DiffInsert: |
| op = fdiff.Add |
| } |
| |
| chunks = append(chunks, &textChunk{d.Text, op}) |
| } |
| |
| return &textFilePatch{ |
| chunks: chunks, |
| from: c.From, |
| to: c.To, |
| }, nil |
| |
| } |
| |
| func filePatch(c *Change) (fdiff.FilePatch, error) { |
| return filePatchWithContext(context.Background(), c) |
| } |
| |
| func fileContent(f *File) (content string, isBinary bool, err error) { |
| if f == nil { |
| return |
| } |
| |
| isBinary, err = f.IsBinary() |
| if err != nil || isBinary { |
| return |
| } |
| |
| content, err = f.Contents() |
| |
| return |
| } |
| |
| // textPatch is an implementation of fdiff.Patch interface |
| type Patch struct { |
| message string |
| filePatches []fdiff.FilePatch |
| } |
| |
| func (t *Patch) FilePatches() []fdiff.FilePatch { |
| return t.filePatches |
| } |
| |
| func (t *Patch) Message() string { |
| return t.message |
| } |
| |
| func (p *Patch) Encode(w io.Writer) error { |
| ue := fdiff.NewUnifiedEncoder(w, fdiff.DefaultContextLines) |
| |
| return ue.Encode(p) |
| } |
| |
| func (p *Patch) Stats() FileStats { |
| return getFileStatsFromFilePatches(p.FilePatches()) |
| } |
| |
| func (p *Patch) String() string { |
| buf := bytes.NewBuffer(nil) |
| err := p.Encode(buf) |
| if err != nil { |
| return fmt.Sprintf("malformed patch: %s", err.Error()) |
| } |
| |
| return buf.String() |
| } |
| |
| // changeEntryWrapper is an implementation of fdiff.File interface |
| type changeEntryWrapper struct { |
| ce ChangeEntry |
| } |
| |
| func (f *changeEntryWrapper) Hash() plumbing.Hash { |
| if !f.ce.TreeEntry.Mode.IsFile() { |
| return plumbing.ZeroHash |
| } |
| |
| return f.ce.TreeEntry.Hash |
| } |
| |
| func (f *changeEntryWrapper) Mode() filemode.FileMode { |
| return f.ce.TreeEntry.Mode |
| } |
| func (f *changeEntryWrapper) Path() string { |
| if !f.ce.TreeEntry.Mode.IsFile() { |
| return "" |
| } |
| |
| return f.ce.Name |
| } |
| |
| func (f *changeEntryWrapper) Empty() bool { |
| return !f.ce.TreeEntry.Mode.IsFile() |
| } |
| |
| // textFilePatch is an implementation of fdiff.FilePatch interface |
| type textFilePatch struct { |
| chunks []fdiff.Chunk |
| from, to ChangeEntry |
| } |
| |
| func (tf *textFilePatch) Files() (from fdiff.File, to fdiff.File) { |
| f := &changeEntryWrapper{tf.from} |
| t := &changeEntryWrapper{tf.to} |
| |
| if !f.Empty() { |
| from = f |
| } |
| |
| if !t.Empty() { |
| to = t |
| } |
| |
| return |
| } |
| |
| func (t *textFilePatch) IsBinary() bool { |
| return len(t.chunks) == 0 |
| } |
| |
| func (t *textFilePatch) Chunks() []fdiff.Chunk { |
| return t.chunks |
| } |
| |
| // textChunk is an implementation of fdiff.Chunk interface |
| type textChunk struct { |
| content string |
| op fdiff.Operation |
| } |
| |
| func (t *textChunk) Content() string { |
| return t.content |
| } |
| |
| func (t *textChunk) Type() fdiff.Operation { |
| return t.op |
| } |
| |
| // FileStat stores the status of changes in content of a file. |
| type FileStat struct { |
| Name string |
| Addition int |
| Deletion int |
| } |
| |
| func (fs FileStat) String() string { |
| return printStat([]FileStat{fs}) |
| } |
| |
| // FileStats is a collection of FileStat. |
| type FileStats []FileStat |
| |
| func (fileStats FileStats) String() string { |
| return printStat(fileStats) |
| } |
| |
| func printStat(fileStats []FileStat) string { |
| padLength := float64(len(" ")) |
| newlineLength := float64(len("\n")) |
| separatorLength := float64(len("|")) |
| // Soft line length limit. The text length calculation below excludes |
| // length of the change number. Adding that would take it closer to 80, |
| // but probably not more than 80, until it's a huge number. |
| lineLength := 72.0 |
| |
| // Get the longest filename and longest total change. |
| var longestLength float64 |
| var longestTotalChange float64 |
| for _, fs := range fileStats { |
| if int(longestLength) < len(fs.Name) { |
| longestLength = float64(len(fs.Name)) |
| } |
| totalChange := fs.Addition + fs.Deletion |
| if int(longestTotalChange) < totalChange { |
| longestTotalChange = float64(totalChange) |
| } |
| } |
| |
| // Parts of the output: |
| // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline> |
| // example: " main.go | 10 +++++++--- " |
| |
| // <pad><filename><pad> |
| leftTextLength := padLength + longestLength + padLength |
| |
| // <pad><number><pad><+++++/-----><newline> |
| // Excluding number length here. |
| rightTextLength := padLength + padLength + newlineLength |
| |
| totalTextArea := leftTextLength + separatorLength + rightTextLength |
| heightOfHistogram := lineLength - totalTextArea |
| |
| // Scale the histogram. |
| var scaleFactor float64 |
| if longestTotalChange > heightOfHistogram { |
| // Scale down to heightOfHistogram. |
| scaleFactor = float64(longestTotalChange / heightOfHistogram) |
| } else { |
| scaleFactor = 1.0 |
| } |
| |
| finalOutput := "" |
| for _, fs := range fileStats { |
| addn := float64(fs.Addition) |
| deln := float64(fs.Deletion) |
| adds := strings.Repeat("+", int(math.Floor(addn/scaleFactor))) |
| dels := strings.Repeat("-", int(math.Floor(deln/scaleFactor))) |
| finalOutput += fmt.Sprintf(" %s | %d %s%s\n", fs.Name, (fs.Addition + fs.Deletion), adds, dels) |
| } |
| |
| return finalOutput |
| } |
| |
| func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats { |
| var fileStats FileStats |
| |
| for _, fp := range filePatches { |
| // ignore empty patches (binary files, submodule refs updates) |
| if len(fp.Chunks()) == 0 { |
| continue |
| } |
| |
| cs := FileStat{} |
| from, to := fp.Files() |
| if from == nil { |
| // New File is created. |
| cs.Name = to.Path() |
| } else if to == nil { |
| // File is deleted. |
| cs.Name = from.Path() |
| } else if from.Path() != to.Path() { |
| // File is renamed. Not supported. |
| // cs.Name = fmt.Sprintf("%s => %s", from.Path(), to.Path()) |
| } else { |
| cs.Name = from.Path() |
| } |
| |
| for _, chunk := range fp.Chunks() { |
| switch chunk.Type() { |
| case fdiff.Add: |
| cs.Addition += strings.Count(chunk.Content(), "\n") |
| case fdiff.Delete: |
| cs.Deletion += strings.Count(chunk.Content(), "\n") |
| } |
| } |
| |
| fileStats = append(fileStats, cs) |
| } |
| |
| return fileStats |
| } |