patch.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package diff
  3. import (
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "kitty/tools/utils"
  8. "kitty/tools/utils/images"
  9. "kitty/tools/utils/shlex"
  10. "os/exec"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. )
  16. var _ = fmt.Print
  17. const GIT_DIFF = `git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --`
  18. const DIFF_DIFF = `diff -p -U _CONTEXT_ --`
  19. var diff_cmd []string
  20. var GitExe = sync.OnceValue(func() string {
  21. return utils.FindExe("git")
  22. })
  23. var DiffExe = sync.OnceValue(func() string {
  24. return utils.FindExe("diff")
  25. })
  26. func find_differ() {
  27. if GitExe() != "git" && exec.Command(GitExe(), "--help").Run() == nil {
  28. diff_cmd, _ = shlex.Split(GIT_DIFF)
  29. } else if DiffExe() != "diff" && exec.Command(DiffExe(), "--help").Run() == nil {
  30. diff_cmd, _ = shlex.Split(DIFF_DIFF)
  31. } else {
  32. diff_cmd = []string{}
  33. }
  34. }
  35. func set_diff_command(q string) error {
  36. switch q {
  37. case "auto":
  38. find_differ()
  39. case "builtin", "":
  40. diff_cmd = []string{}
  41. case "diff":
  42. diff_cmd, _ = shlex.Split(DIFF_DIFF)
  43. case "git":
  44. diff_cmd, _ = shlex.Split(GIT_DIFF)
  45. default:
  46. c, err := shlex.Split(q)
  47. if err != nil {
  48. return err
  49. }
  50. diff_cmd = c
  51. }
  52. return nil
  53. }
  54. type Center struct{ offset, left_size, right_size int }
  55. type Chunk struct {
  56. is_context bool
  57. left_start, right_start int
  58. left_count, right_count int
  59. centers []Center
  60. }
  61. func (self *Chunk) add_line() {
  62. self.right_count++
  63. }
  64. func (self *Chunk) remove_line() {
  65. self.left_count++
  66. }
  67. func (self *Chunk) context_line() {
  68. self.left_count++
  69. self.right_count++
  70. }
  71. func changed_center(left, right string) (ans Center) {
  72. if len(left) > 0 && len(right) > 0 {
  73. ll, rl := len(left), len(right)
  74. ml := utils.Min(ll, rl)
  75. for ; ans.offset < ml && left[ans.offset] == right[ans.offset]; ans.offset++ {
  76. }
  77. suffix_count := 0
  78. for ; suffix_count < ml && left[ll-1-suffix_count] == right[rl-1-suffix_count]; suffix_count++ {
  79. }
  80. ans.left_size = ll - suffix_count - ans.offset
  81. ans.right_size = rl - suffix_count - ans.offset
  82. }
  83. return
  84. }
  85. func (self *Chunk) finalize(left_lines, right_lines []string) {
  86. if !self.is_context && self.left_count == self.right_count {
  87. for i := 0; i < self.left_count; i++ {
  88. self.centers = append(self.centers, changed_center(left_lines[self.left_start+i], right_lines[self.right_start+i]))
  89. }
  90. }
  91. }
  92. type Hunk struct {
  93. left_start, left_count int
  94. right_start, right_count int
  95. title string
  96. added_count, removed_count int
  97. chunks []*Chunk
  98. current_chunk *Chunk
  99. largest_line_number int
  100. }
  101. func (self *Hunk) new_chunk(is_context bool) *Chunk {
  102. left_start, right_start := self.left_start, self.right_start
  103. if len(self.chunks) > 0 {
  104. c := self.chunks[len(self.chunks)-1]
  105. left_start = c.left_start + c.left_count
  106. right_start = c.right_start + c.right_count
  107. }
  108. return &Chunk{is_context: is_context, left_start: left_start, right_start: right_start}
  109. }
  110. func (self *Hunk) ensure_diff_chunk() {
  111. if self.current_chunk == nil || self.current_chunk.is_context {
  112. if self.current_chunk != nil {
  113. self.chunks = append(self.chunks, self.current_chunk)
  114. }
  115. self.current_chunk = self.new_chunk(false)
  116. }
  117. }
  118. func (self *Hunk) ensure_context_chunk() {
  119. if self.current_chunk == nil || !self.current_chunk.is_context {
  120. if self.current_chunk != nil {
  121. self.chunks = append(self.chunks, self.current_chunk)
  122. }
  123. self.current_chunk = self.new_chunk(true)
  124. }
  125. }
  126. func (self *Hunk) add_line() {
  127. self.ensure_diff_chunk()
  128. self.current_chunk.add_line()
  129. self.added_count++
  130. }
  131. func (self *Hunk) remove_line() {
  132. self.ensure_diff_chunk()
  133. self.current_chunk.remove_line()
  134. self.removed_count++
  135. }
  136. func (self *Hunk) context_line() {
  137. self.ensure_context_chunk()
  138. self.current_chunk.context_line()
  139. }
  140. func (self *Hunk) finalize(left_lines, right_lines []string) error {
  141. if self.current_chunk != nil {
  142. self.chunks = append(self.chunks, self.current_chunk)
  143. }
  144. // Sanity check
  145. c := self.chunks[len(self.chunks)-1]
  146. if c.left_start+c.left_count != self.left_start+self.left_count {
  147. return fmt.Errorf("Left side line mismatch %d != %d", c.left_start+c.left_count, self.left_start+self.left_count)
  148. }
  149. if c.right_start+c.right_count != self.right_start+self.right_count {
  150. return fmt.Errorf("Right side line mismatch %d != %d", c.right_start+c.right_count, self.right_start+self.right_count)
  151. }
  152. for _, c := range self.chunks {
  153. c.finalize(left_lines, right_lines)
  154. }
  155. return nil
  156. }
  157. type Patch struct {
  158. all_hunks []*Hunk
  159. largest_line_number, added_count, removed_count int
  160. }
  161. func (self *Patch) Len() int { return len(self.all_hunks) }
  162. func splitlines_like_git(raw string, strip_trailing_lines bool, process_line func(string)) {
  163. sz := len(raw)
  164. if strip_trailing_lines {
  165. for sz > 0 && (raw[sz-1] == '\n' || raw[sz-1] == '\r') {
  166. sz--
  167. }
  168. }
  169. start := 0
  170. for i := 0; i < sz; i++ {
  171. switch raw[i] {
  172. case '\n':
  173. process_line(raw[start:i])
  174. start = i + 1
  175. case '\r':
  176. process_line(raw[start:i])
  177. start = i + 1
  178. if start < sz && raw[start] == '\n' {
  179. i++
  180. start++
  181. }
  182. }
  183. }
  184. if start < sz {
  185. process_line(raw[start:sz])
  186. }
  187. }
  188. func parse_range(x string) (start, count int) {
  189. s, c, found := strings.Cut(x, ",")
  190. start, _ = strconv.Atoi(s)
  191. if start < 0 {
  192. start = -start
  193. }
  194. count = 1
  195. if found {
  196. count, _ = strconv.Atoi(c)
  197. }
  198. return
  199. }
  200. func parse_hunk_header(line string) *Hunk {
  201. parts := strings.SplitN(line, "@@", 3)
  202. linespec := strings.TrimSpace(parts[1])
  203. title := ""
  204. if len(parts) == 3 {
  205. title = strings.TrimSpace(parts[2])
  206. }
  207. left, right, _ := strings.Cut(linespec, " ")
  208. ls, lc := parse_range(left)
  209. rs, rc := parse_range(right)
  210. return &Hunk{
  211. title: title, left_start: ls - 1, left_count: lc, right_start: rs - 1, right_count: rc,
  212. largest_line_number: utils.Max(ls-1+lc, rs-1+rc),
  213. }
  214. }
  215. func parse_patch(raw string, left_lines, right_lines []string) (ans *Patch, err error) {
  216. ans = &Patch{all_hunks: make([]*Hunk, 0, 32)}
  217. var current_hunk *Hunk
  218. splitlines_like_git(raw, true, func(line string) {
  219. if strings.HasPrefix(line, "@@ ") {
  220. current_hunk = parse_hunk_header(line)
  221. ans.all_hunks = append(ans.all_hunks, current_hunk)
  222. } else if current_hunk != nil {
  223. var ch byte
  224. if len(line) > 0 {
  225. ch = line[0]
  226. }
  227. switch ch {
  228. case '+':
  229. current_hunk.add_line()
  230. case '-':
  231. current_hunk.remove_line()
  232. case '\\':
  233. default:
  234. current_hunk.context_line()
  235. }
  236. }
  237. })
  238. for _, h := range ans.all_hunks {
  239. err = h.finalize(left_lines, right_lines)
  240. if err != nil {
  241. return
  242. }
  243. ans.added_count += h.added_count
  244. ans.removed_count += h.removed_count
  245. }
  246. if len(ans.all_hunks) > 0 {
  247. ans.largest_line_number = ans.all_hunks[len(ans.all_hunks)-1].largest_line_number
  248. }
  249. return
  250. }
  251. func run_diff(file1, file2 string, num_of_context_lines int) (ok, is_different bool, patch string, err error) {
  252. // we resolve symlinks because git diff does not follow symlinks, while diff
  253. // does. We want consistent behavior, also for integration with git difftool
  254. // we always want symlinks to be followed.
  255. path1, err := filepath.EvalSymlinks(file1)
  256. if err != nil {
  257. return
  258. }
  259. path2, err := filepath.EvalSymlinks(file2)
  260. if err != nil {
  261. return
  262. }
  263. if len(diff_cmd) == 0 {
  264. data1, err := data_for_path(path1)
  265. if err != nil {
  266. return false, false, "", err
  267. }
  268. data2, err := data_for_path(path2)
  269. if err != nil {
  270. return false, false, "", err
  271. }
  272. patchb := Diff(path1, data1, path2, data2, num_of_context_lines)
  273. if patchb == nil {
  274. return true, false, "", nil
  275. }
  276. return true, len(patchb) > 0, utils.UnsafeBytesToString(patchb), nil
  277. } else {
  278. context := strconv.Itoa(num_of_context_lines)
  279. cmd := utils.Map(func(x string) string {
  280. return strings.ReplaceAll(x, "_CONTEXT_", context)
  281. }, diff_cmd)
  282. cmd = append(cmd, path1, path2)
  283. c := exec.Command(cmd[0], cmd[1:]...)
  284. stdout, stderr := bytes.Buffer{}, bytes.Buffer{}
  285. c.Stdout, c.Stderr = &stdout, &stderr
  286. err = c.Run()
  287. if err != nil {
  288. var e *exec.ExitError
  289. if errors.As(err, &e) && e.ExitCode() == 1 {
  290. return true, true, stdout.String(), nil
  291. }
  292. return false, false, stderr.String(), err
  293. }
  294. return true, false, stdout.String(), nil
  295. }
  296. }
  297. func do_diff(file1, file2 string, context_count int) (ans *Patch, err error) {
  298. ok, _, raw, err := run_diff(file1, file2, context_count)
  299. if !ok {
  300. return nil, fmt.Errorf("Failed to diff %s vs. %s with errors:\n%s", file1, file2, raw)
  301. }
  302. if err != nil {
  303. return
  304. }
  305. left_lines, err := lines_for_path(file1)
  306. if err != nil {
  307. return
  308. }
  309. right_lines, err := lines_for_path(file2)
  310. if err != nil {
  311. return
  312. }
  313. ans, err = parse_patch(raw, left_lines, right_lines)
  314. return
  315. }
  316. type diff_job struct{ file1, file2 string }
  317. func diff(jobs []diff_job, context_count int) (ans map[string]*Patch, err error) {
  318. ans = make(map[string]*Patch)
  319. ctx := images.Context{}
  320. type result struct {
  321. file1, file2 string
  322. err error
  323. patch *Patch
  324. }
  325. results := make(chan result, len(jobs))
  326. ctx.Parallel(0, len(jobs), func(nums <-chan int) {
  327. for i := range nums {
  328. job := jobs[i]
  329. r := result{file1: job.file1, file2: job.file2}
  330. r.patch, r.err = do_diff(job.file1, job.file2, context_count)
  331. results <- r
  332. }
  333. })
  334. close(results)
  335. for r := range results {
  336. if r.err != nil {
  337. return nil, r.err
  338. }
  339. ans[r.file1] = r.patch
  340. }
  341. return ans, nil
  342. }