You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

patch.go 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. package object
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "math"
  9. "strings"
  10. "github.com/go-git/go-git/v5/plumbing"
  11. "github.com/go-git/go-git/v5/plumbing/filemode"
  12. fdiff "github.com/go-git/go-git/v5/plumbing/format/diff"
  13. "github.com/go-git/go-git/v5/utils/diff"
  14. dmp "github.com/sergi/go-diff/diffmatchpatch"
  15. )
  16. var (
  17. ErrCanceled = errors.New("operation canceled")
  18. )
  19. func getPatch(message string, changes ...*Change) (*Patch, error) {
  20. ctx := context.Background()
  21. return getPatchContext(ctx, message, changes...)
  22. }
  23. func getPatchContext(ctx context.Context, message string, changes ...*Change) (*Patch, error) {
  24. var filePatches []fdiff.FilePatch
  25. for _, c := range changes {
  26. select {
  27. case <-ctx.Done():
  28. return nil, ErrCanceled
  29. default:
  30. }
  31. fp, err := filePatchWithContext(ctx, c)
  32. if err != nil {
  33. return nil, err
  34. }
  35. filePatches = append(filePatches, fp)
  36. }
  37. return &Patch{message, filePatches}, nil
  38. }
  39. func filePatchWithContext(ctx context.Context, c *Change) (fdiff.FilePatch, error) {
  40. from, to, err := c.Files()
  41. if err != nil {
  42. return nil, err
  43. }
  44. fromContent, fIsBinary, err := fileContent(from)
  45. if err != nil {
  46. return nil, err
  47. }
  48. toContent, tIsBinary, err := fileContent(to)
  49. if err != nil {
  50. return nil, err
  51. }
  52. if fIsBinary || tIsBinary {
  53. return &textFilePatch{from: c.From, to: c.To}, nil
  54. }
  55. diffs := diff.Do(fromContent, toContent)
  56. var chunks []fdiff.Chunk
  57. for _, d := range diffs {
  58. select {
  59. case <-ctx.Done():
  60. return nil, ErrCanceled
  61. default:
  62. }
  63. var op fdiff.Operation
  64. switch d.Type {
  65. case dmp.DiffEqual:
  66. op = fdiff.Equal
  67. case dmp.DiffDelete:
  68. op = fdiff.Delete
  69. case dmp.DiffInsert:
  70. op = fdiff.Add
  71. }
  72. chunks = append(chunks, &textChunk{d.Text, op})
  73. }
  74. return &textFilePatch{
  75. chunks: chunks,
  76. from: c.From,
  77. to: c.To,
  78. }, nil
  79. }
  80. func filePatch(c *Change) (fdiff.FilePatch, error) {
  81. return filePatchWithContext(context.Background(), c)
  82. }
  83. func fileContent(f *File) (content string, isBinary bool, err error) {
  84. if f == nil {
  85. return
  86. }
  87. isBinary, err = f.IsBinary()
  88. if err != nil || isBinary {
  89. return
  90. }
  91. content, err = f.Contents()
  92. return
  93. }
  94. // Patch is an implementation of fdiff.Patch interface
  95. type Patch struct {
  96. message string
  97. filePatches []fdiff.FilePatch
  98. }
  99. func (t *Patch) FilePatches() []fdiff.FilePatch {
  100. return t.filePatches
  101. }
  102. func (t *Patch) Message() string {
  103. return t.message
  104. }
  105. func (p *Patch) Encode(w io.Writer) error {
  106. ue := fdiff.NewUnifiedEncoder(w, fdiff.DefaultContextLines)
  107. return ue.Encode(p)
  108. }
  109. func (p *Patch) Stats() FileStats {
  110. return getFileStatsFromFilePatches(p.FilePatches())
  111. }
  112. func (p *Patch) String() string {
  113. buf := bytes.NewBuffer(nil)
  114. err := p.Encode(buf)
  115. if err != nil {
  116. return fmt.Sprintf("malformed patch: %s", err.Error())
  117. }
  118. return buf.String()
  119. }
  120. // changeEntryWrapper is an implementation of fdiff.File interface
  121. type changeEntryWrapper struct {
  122. ce ChangeEntry
  123. }
  124. func (f *changeEntryWrapper) Hash() plumbing.Hash {
  125. if !f.ce.TreeEntry.Mode.IsFile() {
  126. return plumbing.ZeroHash
  127. }
  128. return f.ce.TreeEntry.Hash
  129. }
  130. func (f *changeEntryWrapper) Mode() filemode.FileMode {
  131. return f.ce.TreeEntry.Mode
  132. }
  133. func (f *changeEntryWrapper) Path() string {
  134. if !f.ce.TreeEntry.Mode.IsFile() {
  135. return ""
  136. }
  137. return f.ce.Name
  138. }
  139. func (f *changeEntryWrapper) Empty() bool {
  140. return !f.ce.TreeEntry.Mode.IsFile()
  141. }
  142. // textFilePatch is an implementation of fdiff.FilePatch interface
  143. type textFilePatch struct {
  144. chunks []fdiff.Chunk
  145. from, to ChangeEntry
  146. }
  147. func (tf *textFilePatch) Files() (from fdiff.File, to fdiff.File) {
  148. f := &changeEntryWrapper{tf.from}
  149. t := &changeEntryWrapper{tf.to}
  150. if !f.Empty() {
  151. from = f
  152. }
  153. if !t.Empty() {
  154. to = t
  155. }
  156. return
  157. }
  158. func (t *textFilePatch) IsBinary() bool {
  159. return len(t.chunks) == 0
  160. }
  161. func (t *textFilePatch) Chunks() []fdiff.Chunk {
  162. return t.chunks
  163. }
  164. // textChunk is an implementation of fdiff.Chunk interface
  165. type textChunk struct {
  166. content string
  167. op fdiff.Operation
  168. }
  169. func (t *textChunk) Content() string {
  170. return t.content
  171. }
  172. func (t *textChunk) Type() fdiff.Operation {
  173. return t.op
  174. }
  175. // FileStat stores the status of changes in content of a file.
  176. type FileStat struct {
  177. Name string
  178. Addition int
  179. Deletion int
  180. }
  181. func (fs FileStat) String() string {
  182. return printStat([]FileStat{fs})
  183. }
  184. // FileStats is a collection of FileStat.
  185. type FileStats []FileStat
  186. func (fileStats FileStats) String() string {
  187. return printStat(fileStats)
  188. }
  189. func printStat(fileStats []FileStat) string {
  190. padLength := float64(len(" "))
  191. newlineLength := float64(len("\n"))
  192. separatorLength := float64(len("|"))
  193. // Soft line length limit. The text length calculation below excludes
  194. // length of the change number. Adding that would take it closer to 80,
  195. // but probably not more than 80, until it's a huge number.
  196. lineLength := 72.0
  197. // Get the longest filename and longest total change.
  198. var longestLength float64
  199. var longestTotalChange float64
  200. for _, fs := range fileStats {
  201. if int(longestLength) < len(fs.Name) {
  202. longestLength = float64(len(fs.Name))
  203. }
  204. totalChange := fs.Addition + fs.Deletion
  205. if int(longestTotalChange) < totalChange {
  206. longestTotalChange = float64(totalChange)
  207. }
  208. }
  209. // Parts of the output:
  210. // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
  211. // example: " main.go | 10 +++++++--- "
  212. // <pad><filename><pad>
  213. leftTextLength := padLength + longestLength + padLength
  214. // <pad><number><pad><+++++/-----><newline>
  215. // Excluding number length here.
  216. rightTextLength := padLength + padLength + newlineLength
  217. totalTextArea := leftTextLength + separatorLength + rightTextLength
  218. heightOfHistogram := lineLength - totalTextArea
  219. // Scale the histogram.
  220. var scaleFactor float64
  221. if longestTotalChange > heightOfHistogram {
  222. // Scale down to heightOfHistogram.
  223. scaleFactor = longestTotalChange / heightOfHistogram
  224. } else {
  225. scaleFactor = 1.0
  226. }
  227. finalOutput := ""
  228. for _, fs := range fileStats {
  229. addn := float64(fs.Addition)
  230. deln := float64(fs.Deletion)
  231. adds := strings.Repeat("+", int(math.Floor(addn/scaleFactor)))
  232. dels := strings.Repeat("-", int(math.Floor(deln/scaleFactor)))
  233. finalOutput += fmt.Sprintf(" %s | %d %s%s\n", fs.Name, (fs.Addition + fs.Deletion), adds, dels)
  234. }
  235. return finalOutput
  236. }
  237. func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats {
  238. var fileStats FileStats
  239. for _, fp := range filePatches {
  240. // ignore empty patches (binary files, submodule refs updates)
  241. if len(fp.Chunks()) == 0 {
  242. continue
  243. }
  244. cs := FileStat{}
  245. from, to := fp.Files()
  246. if from == nil {
  247. // New File is created.
  248. cs.Name = to.Path()
  249. } else if to == nil {
  250. // File is deleted.
  251. cs.Name = from.Path()
  252. } else if from.Path() != to.Path() {
  253. // File is renamed. Not supported.
  254. // cs.Name = fmt.Sprintf("%s => %s", from.Path(), to.Path())
  255. } else {
  256. cs.Name = from.Path()
  257. }
  258. for _, chunk := range fp.Chunks() {
  259. s := chunk.Content()
  260. if len(s) == 0 {
  261. continue
  262. }
  263. switch chunk.Type() {
  264. case fdiff.Add:
  265. cs.Addition += strings.Count(s, "\n")
  266. if s[len(s)-1] != '\n' {
  267. cs.Addition++
  268. }
  269. case fdiff.Delete:
  270. cs.Deletion += strings.Count(s, "\n")
  271. if s[len(s)-1] != '\n' {
  272. cs.Deletion++
  273. }
  274. }
  275. }
  276. fileStats = append(fileStats, cs)
  277. }
  278. return fileStats
  279. }