collect.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package diff
  3. import (
  4. "crypto/md5"
  5. "fmt"
  6. "io/fs"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "unicode/utf8"
  11. "kitty/tools/utils"
  12. )
  13. var _ = fmt.Print
  14. var path_name_map, remote_dirs map[string]string
  15. var mimetypes_cache, data_cache, hash_cache *utils.LRUCache[string, string]
  16. var size_cache *utils.LRUCache[string, int64]
  17. var lines_cache *utils.LRUCache[string, []string]
  18. var highlighted_lines_cache *utils.LRUCache[string, []string]
  19. var is_text_cache *utils.LRUCache[string, bool]
  20. func init_caches() {
  21. path_name_map = make(map[string]string, 32)
  22. remote_dirs = make(map[string]string, 32)
  23. const sz = 4096
  24. size_cache = utils.NewLRUCache[string, int64](sz)
  25. mimetypes_cache = utils.NewLRUCache[string, string](sz)
  26. data_cache = utils.NewLRUCache[string, string](sz)
  27. is_text_cache = utils.NewLRUCache[string, bool](sz)
  28. lines_cache = utils.NewLRUCache[string, []string](sz)
  29. highlighted_lines_cache = utils.NewLRUCache[string, []string](sz)
  30. hash_cache = utils.NewLRUCache[string, string](sz)
  31. }
  32. func add_remote_dir(val string) {
  33. x := filepath.Base(val)
  34. idx := strings.LastIndex(x, "-")
  35. if idx > -1 {
  36. x = x[idx+1:]
  37. } else {
  38. x = ""
  39. }
  40. remote_dirs[val] = x
  41. }
  42. func mimetype_for_path(path string) string {
  43. return mimetypes_cache.MustGetOrCreate(path, func(path string) string {
  44. mt := utils.GuessMimeTypeWithFileSystemAccess(path)
  45. if mt == "" {
  46. mt = "application/octet-stream"
  47. }
  48. if utils.KnownTextualMimes[mt] {
  49. if _, a, found := strings.Cut(mt, "/"); found {
  50. mt = "text/" + a
  51. }
  52. }
  53. return mt
  54. })
  55. }
  56. func data_for_path(path string) (string, error) {
  57. return data_cache.GetOrCreate(path, func(path string) (string, error) {
  58. ans, err := os.ReadFile(path)
  59. return utils.UnsafeBytesToString(ans), err
  60. })
  61. }
  62. func size_for_path(path string) (int64, error) {
  63. return size_cache.GetOrCreate(path, func(path string) (int64, error) {
  64. s, err := os.Stat(path)
  65. if err != nil {
  66. return 0, err
  67. }
  68. return s.Size(), nil
  69. })
  70. }
  71. func is_image(path string) bool {
  72. return strings.HasPrefix(mimetype_for_path(path), "image/")
  73. }
  74. func is_path_text(path string) bool {
  75. return is_text_cache.MustGetOrCreate(path, func(path string) bool {
  76. if is_image(path) {
  77. return false
  78. }
  79. s1, err := os.Stat(path)
  80. if err == nil {
  81. s2, err := os.Stat("/dev/null")
  82. if err == nil && os.SameFile(s1, s2) {
  83. return false
  84. }
  85. }
  86. d, err := data_for_path(path)
  87. if err != nil {
  88. return false
  89. }
  90. return utf8.ValidString(d)
  91. })
  92. }
  93. func hash_for_path(path string) (string, error) {
  94. return hash_cache.GetOrCreate(path, func(path string) (string, error) {
  95. ans, err := data_for_path(path)
  96. if err != nil {
  97. return "", err
  98. }
  99. hash := md5.Sum(utils.UnsafeStringToBytes(ans))
  100. return utils.UnsafeBytesToString(hash[:]), err
  101. })
  102. }
  103. // Remove all control codes except newlines
  104. func sanitize_control_codes(x string) string {
  105. pat := utils.MustCompile("[\x00-\x09\x0b-\x1f\x7f\u0080-\u009f]")
  106. return pat.ReplaceAllLiteralString(x, "░")
  107. }
  108. func sanitize_tabs_and_carriage_returns(x string) string {
  109. return strings.NewReplacer("\t", conf.Replace_tab_by, "\r", "⏎").Replace(x)
  110. }
  111. func sanitize(x string) string {
  112. return sanitize_control_codes(sanitize_tabs_and_carriage_returns(x))
  113. }
  114. func text_to_lines(text string) []string {
  115. lines := make([]string, 0, 512)
  116. splitlines_like_git(text, false, func(line string) { lines = append(lines, line) })
  117. return lines
  118. }
  119. func lines_for_path(path string) ([]string, error) {
  120. return lines_cache.GetOrCreate(path, func(path string) ([]string, error) {
  121. ans, err := data_for_path(path)
  122. if err != nil {
  123. return nil, err
  124. }
  125. return text_to_lines(sanitize(ans)), nil
  126. })
  127. }
  128. func highlighted_lines_for_path(path string) ([]string, error) {
  129. plain_lines, err := lines_for_path(path)
  130. if err != nil {
  131. return nil, err
  132. }
  133. if ans, found := highlighted_lines_cache.Get(path); found && len(ans) == len(plain_lines) {
  134. return ans, nil
  135. }
  136. return plain_lines, nil
  137. }
  138. type Collection struct {
  139. changes, renames, type_map map[string]string
  140. adds, removes *utils.Set[string]
  141. all_paths []string
  142. paths_to_highlight *utils.Set[string]
  143. added_count, removed_count int
  144. }
  145. func (self *Collection) add_change(left, right string) {
  146. self.changes[left] = right
  147. self.all_paths = append(self.all_paths, left)
  148. self.paths_to_highlight.Add(left)
  149. self.paths_to_highlight.Add(right)
  150. self.type_map[left] = `diff`
  151. }
  152. func (self *Collection) add_rename(left, right string) {
  153. self.renames[left] = right
  154. self.all_paths = append(self.all_paths, left)
  155. self.type_map[left] = `rename`
  156. }
  157. func (self *Collection) add_add(right string) {
  158. self.adds.Add(right)
  159. self.all_paths = append(self.all_paths, right)
  160. self.paths_to_highlight.Add(right)
  161. self.type_map[right] = `add`
  162. if is_path_text(right) {
  163. num, _ := lines_for_path(right)
  164. self.added_count += len(num)
  165. }
  166. }
  167. func (self *Collection) add_removal(left string) {
  168. self.removes.Add(left)
  169. self.all_paths = append(self.all_paths, left)
  170. self.paths_to_highlight.Add(left)
  171. self.type_map[left] = `removal`
  172. if is_path_text(left) {
  173. num, _ := lines_for_path(left)
  174. self.removed_count += len(num)
  175. }
  176. }
  177. func (self *Collection) finalize() {
  178. utils.StableSortWithKey(self.all_paths, func(path string) string {
  179. return path_name_map[path]
  180. })
  181. }
  182. func (self *Collection) Len() int { return len(self.all_paths) }
  183. func (self *Collection) Items() int { return len(self.all_paths) }
  184. func (self *Collection) Apply(f func(path, typ, changed_path string) error) error {
  185. for _, path := range self.all_paths {
  186. typ := self.type_map[path]
  187. changed_path := ""
  188. switch typ {
  189. case "diff":
  190. changed_path = self.changes[path]
  191. case "rename":
  192. changed_path = self.renames[path]
  193. }
  194. if err := f(path, typ, changed_path); err != nil {
  195. return err
  196. }
  197. }
  198. return nil
  199. }
  200. func allowed(path string, patterns ...string) bool {
  201. name := filepath.Base(path)
  202. for _, pat := range patterns {
  203. if matched, err := filepath.Match(pat, name); err == nil && matched {
  204. return false
  205. }
  206. }
  207. return true
  208. }
  209. func remote_hostname(path string) (string, string) {
  210. for q, val := range remote_dirs {
  211. if strings.HasPrefix(path, q) {
  212. return q, val
  213. }
  214. }
  215. return "", ""
  216. }
  217. func resolve_remote_name(path, defval string) string {
  218. remote_dir, rh := remote_hostname(path)
  219. if remote_dir != "" && rh != "" {
  220. r, err := filepath.Rel(remote_dir, path)
  221. if err == nil {
  222. return rh + ":" + r
  223. }
  224. }
  225. return defval
  226. }
  227. func walk(base string, patterns []string, names *utils.Set[string], pmap, path_name_map map[string]string) error {
  228. base, err := filepath.Abs(base)
  229. if err != nil {
  230. return err
  231. }
  232. return filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error {
  233. if err != nil {
  234. return err
  235. }
  236. is_allowed := allowed(path, patterns...)
  237. if !is_allowed {
  238. if d.IsDir() {
  239. return fs.SkipDir
  240. }
  241. return nil
  242. }
  243. if d.IsDir() {
  244. return nil
  245. }
  246. path, err = filepath.Abs(path)
  247. if err != nil {
  248. return err
  249. }
  250. name, err := filepath.Rel(base, path)
  251. if err != nil {
  252. return err
  253. }
  254. if name != "." {
  255. path_name_map[path] = name
  256. names.Add(name)
  257. pmap[name] = path
  258. }
  259. return nil
  260. })
  261. }
  262. func (self *Collection) collect_files(left, right string) error {
  263. left_names, right_names := utils.NewSet[string](16), utils.NewSet[string](16)
  264. left_path_map, right_path_map := make(map[string]string, 16), make(map[string]string, 16)
  265. err := walk(left, conf.Ignore_name, left_names, left_path_map, path_name_map)
  266. if err != nil {
  267. return err
  268. }
  269. if err = walk(right, conf.Ignore_name, right_names, right_path_map, path_name_map); err != nil {
  270. return err
  271. }
  272. common_names := left_names.Intersect(right_names)
  273. changed_names := utils.NewSet[string](common_names.Len())
  274. for n := range common_names.Iterable() {
  275. ld, err := data_for_path(left_path_map[n])
  276. var rd string
  277. if err == nil {
  278. rd, err = data_for_path(right_path_map[n])
  279. }
  280. if err != nil {
  281. return err
  282. }
  283. if ld != rd {
  284. changed_names.Add(n)
  285. self.add_change(left_path_map[n], right_path_map[n])
  286. } else {
  287. if lstat, err := os.Stat(left_path_map[n]); err == nil {
  288. if rstat, err := os.Stat(right_path_map[n]); err == nil {
  289. if lstat.Mode() != rstat.Mode() {
  290. // identical files with only a mode change
  291. changed_names.Add(n)
  292. self.add_change(left_path_map[n], right_path_map[n])
  293. }
  294. }
  295. }
  296. }
  297. }
  298. removed := left_names.Subtract(common_names)
  299. added := right_names.Subtract(common_names)
  300. ahash, rhash := make(map[string]string, added.Len()), make(map[string]string, removed.Len())
  301. for a := range added.Iterable() {
  302. ahash[a], err = hash_for_path(right_path_map[a])
  303. if err != nil {
  304. return err
  305. }
  306. }
  307. for r := range removed.Iterable() {
  308. rhash[r], err = hash_for_path(left_path_map[r])
  309. if err != nil {
  310. return err
  311. }
  312. }
  313. for name, rh := range rhash {
  314. found := false
  315. for n, ah := range ahash {
  316. if ah == rh {
  317. ld, _ := data_for_path(left_path_map[name])
  318. rd, _ := data_for_path(right_path_map[n])
  319. if ld == rd {
  320. self.add_rename(left_path_map[name], right_path_map[n])
  321. added.Discard(n)
  322. found = true
  323. break
  324. }
  325. }
  326. }
  327. if !found {
  328. self.add_removal(left_path_map[name])
  329. }
  330. }
  331. for name := range added.Iterable() {
  332. self.add_add(right_path_map[name])
  333. }
  334. return nil
  335. }
  336. func create_collection(left, right string) (ans *Collection, err error) {
  337. ans = &Collection{
  338. changes: make(map[string]string),
  339. renames: make(map[string]string),
  340. type_map: make(map[string]string),
  341. adds: utils.NewSet[string](32),
  342. removes: utils.NewSet[string](32),
  343. paths_to_highlight: utils.NewSet[string](32),
  344. all_paths: make([]string, 0, 32),
  345. }
  346. left_stat, err := os.Stat(left)
  347. if err != nil {
  348. return nil, err
  349. }
  350. if left_stat.IsDir() {
  351. err = ans.collect_files(left, right)
  352. if err != nil {
  353. return nil, err
  354. }
  355. } else {
  356. pl, err := filepath.Abs(left)
  357. if err != nil {
  358. return nil, err
  359. }
  360. pr, err := filepath.Abs(right)
  361. if err != nil {
  362. return nil, err
  363. }
  364. path_name_map[pl] = resolve_remote_name(pl, left)
  365. path_name_map[pr] = resolve_remote_name(pr, right)
  366. ans.add_change(pl, pr)
  367. }
  368. ans.finalize()
  369. return ans, err
  370. }