magit-diff.el (139224B)
1 ;;; magit-diff.el --- inspect Git diffs -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2010-2021 The Magit Project Contributors 4 ;; 5 ;; You should have received a copy of the AUTHORS.md file which 6 ;; lists all contributors. If not, see http://magit.vc/authors. 7 8 ;; Author: Jonas Bernoulli <jonas@bernoul.li> 9 ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li> 10 11 ;; SPDX-License-Identifier: GPL-3.0-or-later 12 13 ;; Magit is free software; you can redistribute it and/or modify it 14 ;; under the terms of the GNU General Public License as published by 15 ;; the Free Software Foundation; either version 3, or (at your option) 16 ;; any later version. 17 ;; 18 ;; Magit is distributed in the hope that it will be useful, but WITHOUT 19 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 20 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 21 ;; License for more details. 22 ;; 23 ;; You should have received a copy of the GNU General Public License 24 ;; along with Magit. If not, see http://www.gnu.org/licenses. 25 26 ;;; Commentary: 27 28 ;; This library implements support for looking at Git diffs and 29 ;; commits. 30 31 ;;; Code: 32 33 (require 'magit-core) 34 (require 'git-commit) 35 36 (eval-when-compile (require 'ansi-color)) 37 (require 'diff-mode) 38 (require 'smerge-mode) 39 40 ;; For `magit-diff-popup' 41 (declare-function magit-stash-show "magit-stash" (stash &optional args files)) 42 ;; For `magit-diff-visit-file' 43 (declare-function dired-jump "dired-x" (&optional other-window file-name)) 44 (declare-function magit-find-file-noselect "magit-files" (rev file)) 45 (declare-function magit-status-setup-buffer "magit-status" (directory)) 46 ;; For `magit-diff-while-committing' 47 (declare-function magit-commit-message-buffer "magit-commit" ()) 48 ;; For `magit-insert-revision-gravatar' 49 (defvar gravatar-size) 50 ;; For `magit-show-commit' and `magit-diff-show-or-scroll' 51 (declare-function magit-current-blame-chunk "magit-blame" ()) 52 (declare-function magit-blame-mode "magit-blame" (&optional arg)) 53 (defvar magit-blame-mode) 54 ;; For `magit-diff-show-or-scroll' 55 (declare-function git-rebase-current-line "git-rebase" ()) 56 ;; For `magit-diff-unmerged' 57 (declare-function magit-merge-in-progress-p "magit-merge" ()) 58 (declare-function magit--merge-range "magit-merge" (&optional head)) 59 ;; For `magit-diff--dwim' 60 (declare-function forge--pullreq-range "forge-pullreq" 61 (pullreq &optional endpoints)) 62 (declare-function forge--pullreq-ref "forge-pullreq" (pullreq)) 63 ;; For `magit-diff-wash-diff' 64 (declare-function ansi-color-apply-on-region "ansi-color" (begin end)) 65 66 (eval-when-compile 67 (cl-pushnew 'orig-rev eieio--known-slot-names) 68 (cl-pushnew 'action-type eieio--known-slot-names) 69 (cl-pushnew 'target eieio--known-slot-names)) 70 71 ;;; Options 72 ;;;; Diff Mode 73 74 (defgroup magit-diff nil 75 "Inspect and manipulate Git diffs." 76 :link '(info-link "(magit)Diffing") 77 :group 'magit-commands 78 :group 'magit-modes) 79 80 (defcustom magit-diff-mode-hook nil 81 "Hook run after entering Magit-Diff mode." 82 :group 'magit-diff 83 :type 'hook) 84 85 (defcustom magit-diff-sections-hook 86 '(magit-insert-diff 87 magit-insert-xref-buttons) 88 "Hook run to insert sections into a `magit-diff-mode' buffer." 89 :package-version '(magit . "2.3.0") 90 :group 'magit-diff 91 :type 'hook) 92 93 (defcustom magit-diff-expansion-threshold 60 94 "After how many seconds not to expand anymore diffs. 95 96 Except in status buffers, diffs usually start out fully expanded. 97 Because that can take a long time, all diffs that haven't been 98 fontified during a refresh before the threshold defined here are 99 instead displayed with their bodies collapsed. 100 101 Note that this can cause sections that were previously expanded 102 to be collapsed. So you should not pick a very low value here. 103 104 The hook function `magit-diff-expansion-threshold' has to be a 105 member of `magit-section-set-visibility-hook' for this option 106 to have any effect." 107 :package-version '(magit . "2.9.0") 108 :group 'magit-diff 109 :type 'float) 110 111 (defcustom magit-diff-highlight-hunk-body t 112 "Whether to highlight bodies of selected hunk sections. 113 This only has an effect if `magit-diff-highlight' is a 114 member of `magit-section-highlight-hook', which see." 115 :package-version '(magit . "2.1.0") 116 :group 'magit-diff 117 :type 'boolean) 118 119 (defcustom magit-diff-highlight-hunk-region-functions 120 '(magit-diff-highlight-hunk-region-dim-outside 121 magit-diff-highlight-hunk-region-using-overlays) 122 "The functions used to highlight the hunk-internal region. 123 124 `magit-diff-highlight-hunk-region-dim-outside' overlays the outside 125 of the hunk internal selection with a face that causes the added and 126 removed lines to have the same background color as context lines. 127 This function should not be removed from the value of this option. 128 129 `magit-diff-highlight-hunk-region-using-overlays' and 130 `magit-diff-highlight-hunk-region-using-underline' emphasize the 131 region by placing delimiting horizontal lines before and after it. 132 The underline variant was implemented because Eli said that is 133 how we should do it. However the overlay variant actually works 134 better. Also see https://github.com/magit/magit/issues/2758. 135 136 Instead of, or in addition to, using delimiting horizontal lines, 137 to emphasize the boundaries, you may which to emphasize the text 138 itself, using `magit-diff-highlight-hunk-region-using-face'. 139 140 In terminal frames it's not possible to draw lines as the overlay 141 and underline variants normally do, so there they fall back to 142 calling the face function instead." 143 :package-version '(magit . "2.9.0") 144 :set-after '(magit-diff-show-lines-boundaries) 145 :group 'magit-diff 146 :type 'hook 147 :options '(magit-diff-highlight-hunk-region-dim-outside 148 magit-diff-highlight-hunk-region-using-underline 149 magit-diff-highlight-hunk-region-using-overlays 150 magit-diff-highlight-hunk-region-using-face)) 151 152 (defcustom magit-diff-unmarked-lines-keep-foreground t 153 "Whether `magit-diff-highlight-hunk-region-dim-outside' preserves foreground. 154 When this is set to nil, then that function only adjusts the 155 foreground color but added and removed lines outside the region 156 keep their distinct foreground colors." 157 :package-version '(magit . "2.9.0") 158 :group 'magit-diff 159 :type 'boolean) 160 161 (defcustom magit-diff-refine-hunk nil 162 "Whether to show word-granularity differences within diff hunks. 163 164 nil Never show fine differences. 165 t Show fine differences for the current diff hunk only. 166 `all' Show fine differences for all displayed diff hunks." 167 :group 'magit-diff 168 :safe (lambda (val) (memq val '(nil t all))) 169 :type '(choice (const :tag "Never" nil) 170 (const :tag "Current" t) 171 (const :tag "All" all))) 172 173 (defcustom magit-diff-refine-ignore-whitespace smerge-refine-ignore-whitespace 174 "Whether to ignore whitespace changes in word-granularity differences." 175 :package-version '(magit . "3.0.0") 176 :set-after '(smerge-refine-ignore-whitespace) 177 :group 'magit-diff 178 :safe 'booleanp 179 :type 'boolean) 180 181 (put 'magit-diff-refine-hunk 'permanent-local t) 182 183 (defcustom magit-diff-adjust-tab-width nil 184 "Whether to adjust the width of tabs in diffs. 185 186 Determining the correct width can be expensive if it requires 187 opening large and/or many files, so the widths are cached in 188 the variable `magit-diff--tab-width-cache'. Set that to nil 189 to invalidate the cache. 190 191 nil Never adjust tab width. Use `tab-width's value from 192 the Magit buffer itself instead. 193 194 t If the corresponding file-visiting buffer exits, then 195 use `tab-width's value from that buffer. Doing this is 196 cheap, so this value is used even if a corresponding 197 cache entry exists. 198 199 `always' If there is no such buffer, then temporarily visit the 200 file to determine the value. 201 202 NUMBER Like `always', but don't visit files larger than NUMBER 203 bytes." 204 :package-version '(magit . "2.12.0") 205 :group 'magit-diff 206 :type '(choice (const :tag "Never" nil) 207 (const :tag "If file-visiting buffer exists" t) 208 (integer :tag "If file isn't larger than N bytes") 209 (const :tag "Always" always))) 210 211 (defcustom magit-diff-paint-whitespace t 212 "Specify where to highlight whitespace errors. 213 214 nil Never highlight whitespace errors. 215 t Highlight whitespace errors everywhere. 216 `uncommitted' Only highlight whitespace errors in diffs 217 showing uncommitted changes. 218 219 For backward compatibility `status' is treated as a synonym 220 for `uncommitted'. 221 222 The option `magit-diff-paint-whitespace-lines' controls for 223 what lines (added/remove/context) errors are highlighted. 224 225 The options `magit-diff-highlight-trailing' and 226 `magit-diff-highlight-indentation' control what kind of 227 whitespace errors are highlighted." 228 :group 'magit-diff 229 :safe (lambda (val) (memq val '(t nil uncommitted status))) 230 :type '(choice (const :tag "In all diffs" t) 231 (const :tag "Only in uncommitted changes" uncommitted) 232 (const :tag "Never" nil))) 233 234 (defcustom magit-diff-paint-whitespace-lines t 235 "Specify in what kind of lines to highlight whitespace errors. 236 237 t Highlight only in added lines. 238 `both' Highlight in added and removed lines. 239 `all' Highlight in added, removed and context lines." 240 :package-version '(magit . "3.0.0") 241 :group 'magit-diff 242 :safe (lambda (val) (memq val '(t both all))) 243 :type '(choice (const :tag "in added lines" t) 244 (const :tag "in added and removed lines" both) 245 (const :tag "in added, removed and context lines" all))) 246 247 (defcustom magit-diff-highlight-trailing t 248 "Whether to highlight whitespace at the end of a line in diffs. 249 Used only when `magit-diff-paint-whitespace' is non-nil." 250 :group 'magit-diff 251 :safe 'booleanp 252 :type 'boolean) 253 254 (defcustom magit-diff-highlight-indentation nil 255 "Highlight the \"wrong\" indentation style. 256 Used only when `magit-diff-paint-whitespace' is non-nil. 257 258 The value is an alist of the form ((REGEXP . INDENT)...). The 259 path to the current repository is matched against each element 260 in reverse order. Therefore if a REGEXP matches, then earlier 261 elements are not tried. 262 263 If the used INDENT is `tabs', highlight indentation with tabs. 264 If INDENT is an integer, highlight indentation with at least 265 that many spaces. Otherwise, highlight neither." 266 :group 'magit-diff 267 :type `(repeat (cons (string :tag "Directory regexp") 268 (choice (const :tag "Tabs" tabs) 269 (integer :tag "Spaces" :value ,tab-width) 270 (const :tag "Neither" nil))))) 271 272 (defcustom magit-diff-hide-trailing-cr-characters 273 (and (memq system-type '(ms-dos windows-nt)) t) 274 "Whether to hide ^M characters at the end of a line in diffs." 275 :package-version '(magit . "2.6.0") 276 :group 'magit-diff 277 :type 'boolean) 278 279 (defcustom magit-diff-highlight-keywords t 280 "Whether to highlight bracketed keywords in commit messages." 281 :package-version '(magit . "2.12.0") 282 :group 'magit-diff 283 :type 'boolean) 284 285 (defcustom magit-diff-extra-stat-arguments nil 286 "Additional arguments to be used alongside `--stat'. 287 288 A list of zero or more arguments or a function that takes no 289 argument and returns such a list. These arguments are allowed 290 here: `--stat-width', `--stat-name-width', `--stat-graph-width' 291 and `--compact-summary'. See the git-diff(1) manpage." 292 :package-version '(magit . "3.0.0") 293 :group 'magit-diff 294 :type '(radio (function-item magit-diff-use-window-width-as-stat-width) 295 function 296 (list string) 297 (const :tag "None" nil))) 298 299 ;;;; File Diff 300 301 (defcustom magit-diff-buffer-file-locked t 302 "Whether `magit-diff-buffer-file' uses a dedicated buffer." 303 :package-version '(magit . "2.7.0") 304 :group 'magit-commands 305 :group 'magit-diff 306 :type 'boolean) 307 308 ;;;; Revision Mode 309 310 (defgroup magit-revision nil 311 "Inspect and manipulate Git commits." 312 :link '(info-link "(magit)Revision Buffer") 313 :group 'magit-modes) 314 315 (defcustom magit-revision-mode-hook 316 '(bug-reference-mode 317 goto-address-mode) 318 "Hook run after entering Magit-Revision mode." 319 :group 'magit-revision 320 :type 'hook 321 :options '(bug-reference-mode 322 goto-address-mode)) 323 324 (defcustom magit-revision-sections-hook 325 '(magit-insert-revision-tag 326 magit-insert-revision-headers 327 magit-insert-revision-message 328 magit-insert-revision-notes 329 magit-insert-revision-diff 330 magit-insert-xref-buttons) 331 "Hook run to insert sections into a `magit-revision-mode' buffer." 332 :package-version '(magit . "2.3.0") 333 :group 'magit-revision 334 :type 'hook) 335 336 (defcustom magit-revision-headers-format "\ 337 Author: %aN <%aE> 338 AuthorDate: %ad 339 Commit: %cN <%cE> 340 CommitDate: %cd 341 " 342 "Format string used to insert headers in revision buffers. 343 344 All headers in revision buffers are inserted by the section 345 inserter `magit-insert-revision-headers'. Some of the headers 346 are created by calling `git show --format=FORMAT' where FORMAT 347 is the format specified here. Other headers are hard coded or 348 subject to option `magit-revision-insert-related-refs'." 349 :package-version '(magit . "2.3.0") 350 :group 'magit-revision 351 :type 'string) 352 353 (defcustom magit-revision-insert-related-refs t 354 "Whether to show related branches in revision buffers 355 356 `nil' Don't show any related branches. 357 `t' Show related local branches. 358 `all' Show related local and remote branches. 359 `mixed' Show all containing branches and local merged branches." 360 :package-version '(magit . "2.1.0") 361 :group 'magit-revision 362 :type '(choice (const :tag "don't" nil) 363 (const :tag "local only" t) 364 (const :tag "all related" all) 365 (const :tag "all containing, local merged" mixed))) 366 367 (defcustom magit-revision-use-hash-sections 'quicker 368 "Whether to turn hashes inside the commit message into sections. 369 370 If non-nil, then hashes inside the commit message are turned into 371 `commit' sections. There is a trade off to be made between 372 performance and reliability: 373 374 - `slow' calls git for every word to be absolutely sure. 375 - `quick' skips words less than seven characters long. 376 - `quicker' additionally skips words that don't contain a number. 377 - `quickest' uses all words that are at least seven characters 378 long and which contain at least one number as well as at least 379 one letter. 380 381 If nil, then no hashes are turned into sections, but you can 382 still visit the commit at point using \"RET\"." 383 :package-version '(magit . "2.12.0") 384 :group 'magit-revision 385 :type '(choice (const :tag "Use sections, quickest" quickest) 386 (const :tag "Use sections, quicker" quicker) 387 (const :tag "Use sections, quick" quick) 388 (const :tag "Use sections, slow" slow) 389 (const :tag "Don't use sections" nil))) 390 391 (defcustom magit-revision-show-gravatars nil 392 "Whether to show gravatar images in revision buffers. 393 394 If nil, then don't insert any gravatar images. If t, then insert 395 both images. If `author' or `committer', then insert only the 396 respective image. 397 398 If you have customized the option `magit-revision-header-format' 399 and want to insert the images then you might also have to specify 400 where to do so. In that case the value has to be a cons-cell of 401 two regular expressions. The car specifies where to insert the 402 author's image. The top half of the image is inserted right 403 after the matched text, the bottom half on the next line in the 404 same column. The cdr specifies where to insert the committer's 405 image, accordingly. Either the car or the cdr may be nil." 406 :package-version '(magit . "2.3.0") 407 :group 'magit-revision 408 :type '(choice (const :tag "Don't show gravatars" nil) 409 (const :tag "Show gravatars" t) 410 (const :tag "Show author gravatar" author) 411 (const :tag "Show committer gravatar" committer) 412 (cons :tag "Show gravatars using custom pattern." 413 (regexp :tag "Author regexp" "^Author: ") 414 (regexp :tag "Committer regexp" "^Commit: ")))) 415 416 (defcustom magit-revision-use-gravatar-kludge nil 417 "Whether to work around a bug which affects display of gravatars. 418 419 Gravatar images are spliced into two halves which are then 420 displayed on separate lines. On OS X the splicing has a bug in 421 some Emacs builds, which causes the top and bottom halves to be 422 interchanged. Enabling this option works around this issue by 423 interchanging the halves once more, which cancels out the effect 424 of the bug. 425 426 See https://github.com/magit/magit/issues/2265 427 and https://debbugs.gnu.org/cgi/bugreport.cgi?bug=7847. 428 429 Starting with Emacs 26.1 this kludge should not be required for 430 any build." 431 :package-version '(magit . "2.3.0") 432 :group 'magit-revision 433 :type 'boolean) 434 435 (defcustom magit-revision-fill-summary-line nil 436 "Whether to fill excessively long summary lines. 437 438 If this is an integer, then the summary line is filled if it is 439 longer than either the limit specified here or `window-width'. 440 441 You may want to only set this locally in \".dir-locals-2.el\" for 442 repositories known to contain bad commit messages. 443 444 The body of the message is left alone because (a) most people who 445 write excessively long summary lines usually don't add a body and 446 (b) even people who have the decency to wrap their lines may have 447 a good reason to include a long line in the body sometimes." 448 :package-version '(magit . "2.90.0") 449 :group 'magit-revision 450 :type '(choice (const :tag "Don't fill" nil) 451 (integer :tag "Fill if longer than"))) 452 453 (defcustom magit-revision-filter-files-on-follow nil 454 "Whether to honor file filter if log arguments include --follow. 455 456 When a commit is displayed from a log buffer, the resulting 457 revision buffer usually shares the log's file arguments, 458 restricting the diff to those files. However, there's a 459 complication when the log arguments include --follow: if the log 460 follows a file across a rename event, keeping the file 461 restriction would mean showing an empty diff in revision buffers 462 for commits before the rename event. 463 464 When this option is nil, the revision buffer ignores the log's 465 filter if the log arguments include --follow. If non-nil, the 466 log's file filter is always honored." 467 :package-version '(magit . "3.0.0") 468 :group 'magit-revision 469 :type 'boolean) 470 471 ;;;; Visit Commands 472 473 (defcustom magit-diff-visit-previous-blob t 474 "Whether `magit-diff-visit-file' may visit the previous blob. 475 476 When this is t and point is on a removed line in a diff for a 477 committed change, then `magit-diff-visit-file' visits the blob 478 from the last revision which still had that line. 479 480 Currently this is only supported for committed changes, for 481 staged and unstaged changes `magit-diff-visit-file' always 482 visits the file in the working tree." 483 :package-version '(magit . "2.9.0") 484 :group 'magit-diff 485 :type 'boolean) 486 487 (defcustom magit-diff-visit-avoid-head-blob nil 488 "Whether `magit-diff-visit-file' avoids visiting a blob from `HEAD'. 489 490 By default `magit-diff-visit-file' always visits the blob that 491 added the current line, while `magit-diff-visit-worktree-file' 492 visits the respective file in the working tree. For the `HEAD' 493 commit, the former command used to visit the worktree file too, 494 but that made it impossible to visit a blob from `HEAD'. 495 496 When point is on a removed line and that change has not been 497 committed yet, then `magit-diff-visit-file' now visits the last 498 blob that still had that line, which is a blob from `HEAD'. 499 Previously this function used to visit the worktree file not 500 only for added lines but also for such removed lines. 501 502 If you prefer the old behaviors, then set this to t." 503 :package-version '(magit . "3.0.0") 504 :group 'magit-diff 505 :type 'boolean) 506 507 ;;; Faces 508 509 (defface magit-diff-file-heading 510 `((t ,@(and (>= emacs-major-version 27) '(:extend t)) 511 :weight bold)) 512 "Face for diff file headings." 513 :group 'magit-faces) 514 515 (defface magit-diff-file-heading-highlight 516 `((t ,@(and (>= emacs-major-version 27) '(:extend t)) 517 :inherit magit-section-highlight)) 518 "Face for current diff file headings." 519 :group 'magit-faces) 520 521 (defface magit-diff-file-heading-selection 522 `((((class color) (background light)) 523 ,@(and (>= emacs-major-version 27) '(:extend t)) 524 :inherit magit-diff-file-heading-highlight 525 :foreground "salmon4") 526 (((class color) (background dark)) 527 ,@(and (>= emacs-major-version 27) '(:extend t)) 528 :inherit magit-diff-file-heading-highlight 529 :foreground "LightSalmon3")) 530 "Face for selected diff file headings." 531 :group 'magit-faces) 532 533 (defface magit-diff-hunk-heading 534 `((((class color) (background light)) 535 ,@(and (>= emacs-major-version 27) '(:extend t)) 536 :background "grey80" 537 :foreground "grey30") 538 (((class color) (background dark)) 539 ,@(and (>= emacs-major-version 27) '(:extend t)) 540 :background "grey25" 541 :foreground "grey70")) 542 "Face for diff hunk headings." 543 :group 'magit-faces) 544 545 (defface magit-diff-hunk-heading-highlight 546 `((((class color) (background light)) 547 ,@(and (>= emacs-major-version 27) '(:extend t)) 548 :background "grey75" 549 :foreground "grey30") 550 (((class color) (background dark)) 551 ,@(and (>= emacs-major-version 27) '(:extend t)) 552 :background "grey35" 553 :foreground "grey70")) 554 "Face for current diff hunk headings." 555 :group 'magit-faces) 556 557 (defface magit-diff-hunk-heading-selection 558 `((((class color) (background light)) 559 ,@(and (>= emacs-major-version 27) '(:extend t)) 560 :inherit magit-diff-hunk-heading-highlight 561 :foreground "salmon4") 562 (((class color) (background dark)) 563 ,@(and (>= emacs-major-version 27) '(:extend t)) 564 :inherit magit-diff-hunk-heading-highlight 565 :foreground "LightSalmon3")) 566 "Face for selected diff hunk headings." 567 :group 'magit-faces) 568 569 (defface magit-diff-hunk-region 570 `((t :inherit bold 571 ,@(and (>= emacs-major-version 27) 572 (list :extend (ignore-errors (face-attribute 'region :extend)))))) 573 "Face used by `magit-diff-highlight-hunk-region-using-face'. 574 575 This face is overlaid over text that uses other hunk faces, 576 and those normally set the foreground and background colors. 577 The `:foreground' and especially the `:background' properties 578 should be avoided here. Setting the latter would cause the 579 loss of information. Good properties to set here are `:weight' 580 and `:slant'." 581 :group 'magit-faces) 582 583 (defface magit-diff-revision-summary 584 '((t :inherit magit-diff-hunk-heading)) 585 "Face for commit message summaries." 586 :group 'magit-faces) 587 588 (defface magit-diff-revision-summary-highlight 589 '((t :inherit magit-diff-hunk-heading-highlight)) 590 "Face for highlighted commit message summaries." 591 :group 'magit-faces) 592 593 (defface magit-diff-lines-heading 594 `((((class color) (background light)) 595 ,@(and (>= emacs-major-version 27) '(:extend t)) 596 :inherit magit-diff-hunk-heading-highlight 597 :background "LightSalmon3") 598 (((class color) (background dark)) 599 ,@(and (>= emacs-major-version 27) '(:extend t)) 600 :inherit magit-diff-hunk-heading-highlight 601 :foreground "grey80" 602 :background "salmon4")) 603 "Face for diff hunk heading when lines are marked." 604 :group 'magit-faces) 605 606 (defface magit-diff-lines-boundary 607 `((t ,@(and (>= emacs-major-version 27) '(:extend t)) ; !important 608 :inherit magit-diff-lines-heading)) 609 "Face for boundary of marked lines in diff hunk." 610 :group 'magit-faces) 611 612 (defface magit-diff-conflict-heading 613 '((t :inherit magit-diff-hunk-heading)) 614 "Face for conflict markers." 615 :group 'magit-faces) 616 617 (defface magit-diff-added 618 `((((class color) (background light)) 619 ,@(and (>= emacs-major-version 27) '(:extend t)) 620 :background "#ddffdd" 621 :foreground "#22aa22") 622 (((class color) (background dark)) 623 ,@(and (>= emacs-major-version 27) '(:extend t)) 624 :background "#335533" 625 :foreground "#ddffdd")) 626 "Face for lines in a diff that have been added." 627 :group 'magit-faces) 628 629 (defface magit-diff-removed 630 `((((class color) (background light)) 631 ,@(and (>= emacs-major-version 27) '(:extend t)) 632 :background "#ffdddd" 633 :foreground "#aa2222") 634 (((class color) (background dark)) 635 ,@(and (>= emacs-major-version 27) '(:extend t)) 636 :background "#553333" 637 :foreground "#ffdddd")) 638 "Face for lines in a diff that have been removed." 639 :group 'magit-faces) 640 641 (defface magit-diff-our 642 '((t :inherit magit-diff-removed)) 643 "Face for lines in a diff for our side in a conflict." 644 :group 'magit-faces) 645 646 (defface magit-diff-base 647 `((((class color) (background light)) 648 ,@(and (>= emacs-major-version 27) '(:extend t)) 649 :background "#ffffcc" 650 :foreground "#aaaa11") 651 (((class color) (background dark)) 652 ,@(and (>= emacs-major-version 27) '(:extend t)) 653 :background "#555522" 654 :foreground "#ffffcc")) 655 "Face for lines in a diff for the base side in a conflict." 656 :group 'magit-faces) 657 658 (defface magit-diff-their 659 '((t :inherit magit-diff-added)) 660 "Face for lines in a diff for their side in a conflict." 661 :group 'magit-faces) 662 663 (defface magit-diff-context 664 `((((class color) (background light)) 665 ,@(and (>= emacs-major-version 27) '(:extend t)) 666 :foreground "grey50") 667 (((class color) (background dark)) 668 ,@(and (>= emacs-major-version 27) '(:extend t)) 669 :foreground "grey70")) 670 "Face for lines in a diff that are unchanged." 671 :group 'magit-faces) 672 673 (defface magit-diff-added-highlight 674 `((((class color) (background light)) 675 ,@(and (>= emacs-major-version 27) '(:extend t)) 676 :background "#cceecc" 677 :foreground "#22aa22") 678 (((class color) (background dark)) 679 ,@(and (>= emacs-major-version 27) '(:extend t)) 680 :background "#336633" 681 :foreground "#cceecc")) 682 "Face for lines in a diff that have been added." 683 :group 'magit-faces) 684 685 (defface magit-diff-removed-highlight 686 `((((class color) (background light)) 687 ,@(and (>= emacs-major-version 27) '(:extend t)) 688 :background "#eecccc" 689 :foreground "#aa2222") 690 (((class color) (background dark)) 691 ,@(and (>= emacs-major-version 27) '(:extend t)) 692 :background "#663333" 693 :foreground "#eecccc")) 694 "Face for lines in a diff that have been removed." 695 :group 'magit-faces) 696 697 (defface magit-diff-our-highlight 698 '((t :inherit magit-diff-removed-highlight)) 699 "Face for lines in a diff for our side in a conflict." 700 :group 'magit-faces) 701 702 (defface magit-diff-base-highlight 703 `((((class color) (background light)) 704 ,@(and (>= emacs-major-version 27) '(:extend t)) 705 :background "#eeeebb" 706 :foreground "#aaaa11") 707 (((class color) (background dark)) 708 ,@(and (>= emacs-major-version 27) '(:extend t)) 709 :background "#666622" 710 :foreground "#eeeebb")) 711 "Face for lines in a diff for the base side in a conflict." 712 :group 'magit-faces) 713 714 (defface magit-diff-their-highlight 715 '((t :inherit magit-diff-added-highlight)) 716 "Face for lines in a diff for their side in a conflict." 717 :group 'magit-faces) 718 719 (defface magit-diff-context-highlight 720 `((((class color) (background light)) 721 ,@(and (>= emacs-major-version 27) '(:extend t)) 722 :background "grey95" 723 :foreground "grey50") 724 (((class color) (background dark)) 725 ,@(and (>= emacs-major-version 27) '(:extend t)) 726 :background "grey20" 727 :foreground "grey70")) 728 "Face for lines in the current context in a diff." 729 :group 'magit-faces) 730 731 (defface magit-diff-whitespace-warning 732 '((t :inherit trailing-whitespace)) 733 "Face for highlighting whitespace errors added lines." 734 :group 'magit-faces) 735 736 (defface magit-diffstat-added 737 '((((class color) (background light)) :foreground "#22aa22") 738 (((class color) (background dark)) :foreground "#448844")) 739 "Face for plus sign in diffstat." 740 :group 'magit-faces) 741 742 (defface magit-diffstat-removed 743 '((((class color) (background light)) :foreground "#aa2222") 744 (((class color) (background dark)) :foreground "#aa4444")) 745 "Face for minus sign in diffstat." 746 :group 'magit-faces) 747 748 ;;; Arguments 749 ;;;; Prefix Classes 750 751 (defclass magit-diff-prefix (transient-prefix) 752 ((history-key :initform 'magit-diff) 753 (major-mode :initform 'magit-diff-mode))) 754 755 (defclass magit-diff-refresh-prefix (magit-diff-prefix) 756 ((history-key :initform 'magit-diff) 757 (major-mode :initform nil))) 758 759 ;;;; Prefix Methods 760 761 (cl-defmethod transient-init-value ((obj magit-diff-prefix)) 762 (pcase-let ((`(,args ,files) 763 (magit-diff--get-value 'magit-diff-mode 764 magit-prefix-use-buffer-arguments))) 765 (unless (eq transient-current-command 'magit-dispatch) 766 (when-let ((file (magit-file-relative-name))) 767 (setq files (list file)))) 768 (oset obj value (if files `(("--" ,@files) ,args) args)))) 769 770 (cl-defmethod transient-init-value ((obj magit-diff-refresh-prefix)) 771 (oset obj value (if magit-buffer-diff-files 772 `(("--" ,@magit-buffer-diff-files) 773 ,magit-buffer-diff-args) 774 magit-buffer-diff-args))) 775 776 (cl-defmethod transient-set-value ((obj magit-diff-prefix)) 777 (magit-diff--set-value obj)) 778 779 (cl-defmethod transient-save-value ((obj magit-diff-prefix)) 780 (magit-diff--set-value obj 'save)) 781 782 ;;;; Argument Access 783 784 (defun magit-diff-arguments (&optional mode) 785 "Return the current diff arguments." 786 (if (memq transient-current-command '(magit-diff magit-diff-refresh)) 787 (pcase-let ((`(,args ,alist) 788 (-separate #'atom (transient-get-value)))) 789 (list args (cdr (assoc "--" alist)))) 790 (magit-diff--get-value (or mode 'magit-diff-mode)))) 791 792 (defun magit-diff--get-value (mode &optional use-buffer-args) 793 (unless use-buffer-args 794 (setq use-buffer-args magit-direct-use-buffer-arguments)) 795 (let (args files) 796 (cond 797 ((and (memq use-buffer-args '(always selected current)) 798 (eq major-mode mode)) 799 (setq args magit-buffer-diff-args) 800 (setq files magit-buffer-diff-files)) 801 ((and (memq use-buffer-args '(always selected)) 802 (when-let ((buffer (magit-get-mode-buffer 803 mode nil 804 (eq use-buffer-args 'selected)))) 805 (setq args (buffer-local-value 'magit-buffer-diff-args buffer)) 806 (setq files (buffer-local-value 'magit-buffer-diff-files buffer)) 807 t))) 808 ((plist-member (symbol-plist mode) 'magit-diff-current-arguments) 809 (setq args (get mode 'magit-diff-current-arguments))) 810 ((when-let ((elt (assq (intern (format "magit-diff:%s" mode)) 811 transient-values))) 812 (setq args (cdr elt)) 813 t)) 814 (t 815 (setq args (get mode 'magit-diff-default-arguments)))) 816 (list args files))) 817 818 (defun magit-diff--set-value (obj &optional save) 819 (pcase-let* ((obj (oref obj prototype)) 820 (mode (or (oref obj major-mode) major-mode)) 821 (key (intern (format "magit-diff:%s" mode))) 822 (`(,args ,alist) 823 (-separate #'atom (transient-get-value))) 824 (files (cdr (assoc "--" alist)))) 825 (put mode 'magit-diff-current-arguments args) 826 (when save 827 (setf (alist-get key transient-values) args) 828 (transient-save-values)) 829 (transient--history-push obj) 830 (setq magit-buffer-diff-args args) 831 (setq magit-buffer-diff-files files) 832 (magit-refresh))) 833 834 ;;; Section Classes 835 836 (defclass magit-file-section (magit-section) 837 ((keymap :initform 'magit-file-section-map) 838 (source :initform nil) 839 (header :initform nil))) 840 841 (defclass magit-module-section (magit-file-section) 842 ((keymap :initform 'magit-hunk-section-map))) 843 844 (defclass magit-hunk-section (magit-section) 845 ((keymap :initform 'magit-hunk-section-map) 846 (refined :initform nil) 847 (combined :initform nil) 848 (from-range :initform nil) 849 (from-ranges :initform nil) 850 (to-range :initform nil) 851 (about :initform nil))) 852 853 (setf (alist-get 'hunk magit--section-type-alist) 'magit-hunk-section) 854 (setf (alist-get 'module magit--section-type-alist) 'magit-module-section) 855 (setf (alist-get 'file magit--section-type-alist) 'magit-file-section) 856 857 ;;; Commands 858 ;;;; Prefix Commands 859 860 ;;;###autoload (autoload 'magit-diff "magit-diff" nil t) 861 (transient-define-prefix magit-diff () 862 "Show changes between different versions." 863 :man-page "git-diff" 864 :class 'magit-diff-prefix 865 ["Limit arguments" 866 (magit:--) 867 (magit-diff:--ignore-submodules) 868 ("-b" "Ignore whitespace changes" ("-b" "--ignore-space-change")) 869 ("-w" "Ignore all whitespace" ("-w" "--ignore-all-space")) 870 (5 "-D" "Omit preimage for deletes" ("-D" "--irreversible-delete"))] 871 ["Context arguments" 872 (magit-diff:-U) 873 ("-W" "Show surrounding functions" ("-W" "--function-context"))] 874 ["Tune arguments" 875 (magit-diff:--diff-algorithm) 876 (magit-diff:-M) 877 (magit-diff:-C) 878 ("-x" "Disallow external diff drivers" "--no-ext-diff") 879 ("-s" "Show stats" "--stat") 880 ("=g" "Show signature" "--show-signature") 881 (5 "-R" "Reverse sides" "-R") 882 (5 magit-diff:--color-moved) 883 (5 magit-diff:--color-moved-ws)] 884 ["Actions" 885 [("d" "Dwim" magit-diff-dwim) 886 ("r" "Diff range" magit-diff-range) 887 ("p" "Diff paths" magit-diff-paths)] 888 [("u" "Diff unstaged" magit-diff-unstaged) 889 ("s" "Diff staged" magit-diff-staged) 890 ("w" "Diff worktree" magit-diff-working-tree)] 891 [("c" "Show commit" magit-show-commit) 892 ("t" "Show stash" magit-stash-show)]]) 893 894 ;;;###autoload (autoload 'magit-diff-refresh "magit-diff" nil t) 895 (transient-define-prefix magit-diff-refresh () 896 "Change the arguments used for the diff(s) in the current buffer." 897 :man-page "git-diff" 898 :class 'magit-diff-refresh-prefix 899 ["Limit arguments" 900 (magit:--) 901 (magit-diff:--ignore-submodules) 902 ("-b" "Ignore whitespace changes" ("-b" "--ignore-space-change")) 903 ("-w" "Ignore all whitespace" ("-w" "--ignore-all-space")) 904 (5 "-D" "Omit preimage for deletes" ("-D" "--irreversible-delete"))] 905 ["Context arguments" 906 (magit-diff:-U) 907 ("-W" "Show surrounding functions" ("-W" "--function-context"))] 908 ["Tune arguments" 909 (magit-diff:--diff-algorithm) 910 (magit-diff:-M) 911 (magit-diff:-C) 912 ("-x" "Disallow external diff drivers" "--no-ext-diff") 913 ("-s" "Show stats" "--stat" 914 :if-derived magit-diff-mode) 915 ("=g" "Show signature" "--show-signature" 916 :if-derived magit-diff-mode) 917 (5 "-R" "Reverse sides" "-R" 918 :if-derived magit-diff-mode) 919 (5 magit-diff:--color-moved) 920 (5 magit-diff:--color-moved-ws)] 921 [["Refresh" 922 ("g" "buffer" magit-diff-refresh) 923 ("s" "buffer and set defaults" transient-set :transient nil) 924 ("w" "buffer and save defaults" transient-save :transient nil)] 925 ["Toggle" 926 ("t" "hunk refinement" magit-diff-toggle-refine-hunk) 927 ("F" "file filter" magit-diff-toggle-file-filter) 928 ("b" "buffer lock" magit-toggle-buffer-lock 929 :if-mode (magit-diff-mode magit-revision-mode magit-stash-mode))] 930 [:if-mode magit-diff-mode 931 :description "Do" 932 ("r" "switch range type" magit-diff-switch-range-type) 933 ("f" "flip revisions" magit-diff-flip-revs)]] 934 (interactive) 935 (if (not (eq transient-current-command 'magit-diff-refresh)) 936 (transient-setup 'magit-diff-refresh) 937 (pcase-let ((`(,args ,files) (magit-diff-arguments))) 938 (setq magit-buffer-diff-args args) 939 (setq magit-buffer-diff-files files)) 940 (magit-refresh))) 941 942 ;;;; Infix Commands 943 944 (transient-define-argument magit:-- () 945 :description "Limit to files" 946 :class 'transient-files 947 :key "--" 948 :argument "--" 949 :prompt "Limit to file(s): " 950 :reader 'magit-read-files 951 :multi-value t) 952 953 (defun magit-read-files (prompt initial-input history) 954 (magit-completing-read-multiple* prompt 955 (magit-list-files) 956 nil nil 957 (or initial-input (magit-file-at-point)) 958 history)) 959 960 (transient-define-argument magit-diff:-U () 961 :description "Context lines" 962 :class 'transient-option 963 :argument "-U" 964 :reader 'transient-read-number-N0) 965 966 (transient-define-argument magit-diff:-M () 967 :description "Detect renames" 968 :class 'transient-option 969 :argument "-M" 970 :allow-empty t 971 :reader 'transient-read-number-N+) 972 973 (transient-define-argument magit-diff:-C () 974 :description "Detect copies" 975 :class 'transient-option 976 :argument "-C" 977 :allow-empty t 978 :reader 'transient-read-number-N+) 979 980 (transient-define-argument magit-diff:--diff-algorithm () 981 :description "Diff algorithm" 982 :class 'transient-option 983 :key "-A" 984 :argument "--diff-algorithm=" 985 :reader 'magit-diff-select-algorithm) 986 987 (defun magit-diff-select-algorithm (&rest _ignore) 988 (magit-read-char-case nil t 989 (?d "[d]efault" "default") 990 (?m "[m]inimal" "minimal") 991 (?p "[p]atience" "patience") 992 (?h "[h]istogram" "histogram"))) 993 994 (transient-define-argument magit-diff:--ignore-submodules () 995 :description "Ignore submodules" 996 :class 'transient-option 997 :key "-i" 998 :argument "--ignore-submodules=" 999 :reader 'magit-diff-select-ignore-submodules) 1000 1001 (defun magit-diff-select-ignore-submodules (&rest _ignored) 1002 (magit-read-char-case "Ignore submodules " t 1003 (?u "[u]ntracked" "untracked") 1004 (?d "[d]irty" "dirty") 1005 (?a "[a]ll" "all"))) 1006 1007 (transient-define-argument magit-diff:--color-moved () 1008 :description "Color moved lines" 1009 :class 'transient-option 1010 :key "-m" 1011 :argument "--color-moved=" 1012 :reader 'magit-diff-select-color-moved-mode) 1013 1014 (defun magit-diff-select-color-moved-mode (&rest _ignore) 1015 (magit-read-char-case "Color moved " t 1016 (?d "[d]efault" "default") 1017 (?p "[p]lain" "plain") 1018 (?b "[b]locks" "blocks") 1019 (?z "[z]ebra" "zebra") 1020 (?Z "[Z] dimmed-zebra" "dimmed-zebra"))) 1021 1022 (transient-define-argument magit-diff:--color-moved-ws () 1023 :description "Whitespace treatment for --color-moved" 1024 :class 'transient-option 1025 :key "=w" 1026 :argument "--color-moved-ws=" 1027 :reader 'magit-diff-select-color-moved-ws-mode) 1028 1029 (defun magit-diff-select-color-moved-ws-mode (&rest _ignore) 1030 (magit-read-char-case "Ignore whitespace " t 1031 (?i "[i]ndentation" "allow-indentation-change") 1032 (?e "[e]nd of line" "ignore-space-at-eol") 1033 (?s "[s]pace change" "ignore-space-change") 1034 (?a "[a]ll space" "ignore-all-space") 1035 (?n "[n]o" "no"))) 1036 1037 ;;;; Setup Commands 1038 1039 ;;;###autoload 1040 (defun magit-diff-dwim (&optional args files) 1041 "Show changes for the thing at point." 1042 (interactive (magit-diff-arguments)) 1043 (pcase (magit-diff--dwim) 1044 (`unmerged (magit-diff-unmerged args files)) 1045 (`unstaged (magit-diff-unstaged args files)) 1046 (`staged 1047 (let ((file (magit-file-at-point))) 1048 (if (and file (equal (cddr (car (magit-file-status file))) '(?D ?U))) 1049 ;; File was deleted by us and modified by them. Show the latter. 1050 (magit-diff-unmerged args (list file)) 1051 (magit-diff-staged nil args files)))) 1052 (`(commit . ,value) 1053 (magit-diff-range (format "%s^..%s" value value) args files)) 1054 (`(stash . ,value) (magit-stash-show value args)) 1055 ((and range (pred stringp)) 1056 (magit-diff-range range args files)) 1057 (_ 1058 (call-interactively #'magit-diff-range)))) 1059 1060 (defun magit-diff--dwim () 1061 "Return information for performing DWIM diff. 1062 1063 The information can be in three forms: 1064 1. TYPE 1065 A symbol describing a type of diff where no additional information 1066 is needed to generate the diff. Currently, this includes `staged', 1067 `unstaged' and `unmerged'. 1068 2. (TYPE . VALUE) 1069 Like #1 but the diff requires additional information, which is 1070 given by VALUE. Currently, this includes `commit' and `stash', 1071 where VALUE is the given commit or stash, respectively. 1072 3. RANGE 1073 A string indicating a diff range. 1074 1075 If no DWIM context is found, nil is returned." 1076 (cond 1077 ((--when-let (magit-region-values '(commit branch) t) 1078 (deactivate-mark) 1079 (concat (car (last it)) ".." (car it)))) 1080 (magit-buffer-refname 1081 (cons 'commit magit-buffer-refname)) 1082 ((derived-mode-p 'magit-stash-mode) 1083 (cons 'commit 1084 (magit-section-case 1085 (commit (oref it value)) 1086 (file (thread-first it 1087 (oref parent) 1088 (oref value))) 1089 (hunk (thread-first it 1090 (oref parent) 1091 (oref parent) 1092 (oref value)))))) 1093 ((derived-mode-p 'magit-revision-mode) 1094 (cons 'commit magit-buffer-revision)) 1095 ((derived-mode-p 'magit-diff-mode) 1096 magit-buffer-range) 1097 (t 1098 (magit-section-case 1099 ([* unstaged] 'unstaged) 1100 ([* staged] 'staged) 1101 (unmerged 'unmerged) 1102 (unpushed (oref it value)) 1103 (unpulled (oref it value)) 1104 (branch (let ((current (magit-get-current-branch)) 1105 (atpoint (oref it value))) 1106 (if (equal atpoint current) 1107 (--if-let (magit-get-upstream-branch) 1108 (format "%s...%s" it current) 1109 (if (magit-anything-modified-p) 1110 current 1111 (cons 'commit current))) 1112 (format "%s...%s" 1113 (or current "HEAD") 1114 atpoint)))) 1115 (commit (cons 'commit (oref it value))) 1116 ([file commit] (cons 'commit (oref (oref it parent) value))) 1117 ([hunk file commit] 1118 (cons 'commit (oref (oref (oref it parent) parent) value))) 1119 (stash (cons 'stash (oref it value))) 1120 (pullreq (forge--pullreq-range (oref it value) t)))))) 1121 1122 (defun magit-diff-read-range-or-commit (prompt &optional secondary-default mbase) 1123 "Read range or revision with special diff range treatment. 1124 If MBASE is non-nil, prompt for which rev to place at the end of 1125 a \"revA...revB\" range. Otherwise, always construct 1126 \"revA..revB\" range." 1127 (--if-let (magit-region-values '(commit branch) t) 1128 (let ((revA (car (last it))) 1129 (revB (car it))) 1130 (deactivate-mark) 1131 (if mbase 1132 (let ((base (magit-git-string "merge-base" revA revB))) 1133 (cond 1134 ((string= (magit-rev-parse revA) base) 1135 (format "%s..%s" revA revB)) 1136 ((string= (magit-rev-parse revB) base) 1137 (format "%s..%s" revB revA)) 1138 (t 1139 (let ((main (magit-completing-read "View changes along" 1140 (list revA revB) 1141 nil t nil nil revB))) 1142 (format "%s...%s" 1143 (if (string= main revB) revA revB) main))))) 1144 (format "%s..%s" revA revB))) 1145 (magit-read-range prompt 1146 (or (pcase (magit-diff--dwim) 1147 (`(commit . ,value) 1148 (format "%s^..%s" value value)) 1149 ((and range (pred stringp)) 1150 range)) 1151 secondary-default 1152 (magit-get-current-branch))))) 1153 1154 ;;;###autoload 1155 (defun magit-diff-range (rev-or-range &optional args files) 1156 "Show differences between two commits. 1157 1158 REV-OR-RANGE should be a range or a single revision. If it is a 1159 revision, then show changes in the working tree relative to that 1160 revision. If it is a range, but one side is omitted, then show 1161 changes relative to `HEAD'. 1162 1163 If the region is active, use the revisions on the first and last 1164 line of the region as the two sides of the range. With a prefix 1165 argument, instead of diffing the revisions, choose a revision to 1166 view changes along, starting at the common ancestor of both 1167 revisions (i.e., use a \"...\" range)." 1168 (interactive (cons (magit-diff-read-range-or-commit "Diff for range" 1169 nil current-prefix-arg) 1170 (magit-diff-arguments))) 1171 (magit-diff-setup-buffer rev-or-range nil args files)) 1172 1173 ;;;###autoload 1174 (defun magit-diff-working-tree (&optional rev args files) 1175 "Show changes between the current working tree and the `HEAD' commit. 1176 With a prefix argument show changes between the working tree and 1177 a commit read from the minibuffer." 1178 (interactive 1179 (cons (and current-prefix-arg 1180 (magit-read-branch-or-commit "Diff working tree and commit")) 1181 (magit-diff-arguments))) 1182 (magit-diff-setup-buffer (or rev "HEAD") nil args files)) 1183 1184 ;;;###autoload 1185 (defun magit-diff-staged (&optional rev args files) 1186 "Show changes between the index and the `HEAD' commit. 1187 With a prefix argument show changes between the index and 1188 a commit read from the minibuffer." 1189 (interactive 1190 (cons (and current-prefix-arg 1191 (magit-read-branch-or-commit "Diff index and commit")) 1192 (magit-diff-arguments))) 1193 (magit-diff-setup-buffer rev "--cached" args files)) 1194 1195 ;;;###autoload 1196 (defun magit-diff-unstaged (&optional args files) 1197 "Show changes between the working tree and the index." 1198 (interactive (magit-diff-arguments)) 1199 (magit-diff-setup-buffer nil nil args files)) 1200 1201 ;;;###autoload 1202 (defun magit-diff-unmerged (&optional args files) 1203 "Show changes that are being merged." 1204 (interactive (magit-diff-arguments)) 1205 (unless (magit-merge-in-progress-p) 1206 (user-error "No merge is in progress")) 1207 (magit-diff-setup-buffer (magit--merge-range) nil args files)) 1208 1209 ;;;###autoload 1210 (defun magit-diff-while-committing (&optional args) 1211 "While committing, show the changes that are about to be committed. 1212 While amending, invoking the command again toggles between 1213 showing just the new changes or all the changes that will 1214 be committed." 1215 (interactive (list (car (magit-diff-arguments)))) 1216 (unless (magit-commit-message-buffer) 1217 (user-error "No commit in progress")) 1218 (let ((magit-display-buffer-noselect t)) 1219 (if-let ((diff-buf (magit-get-mode-buffer 'magit-diff-mode 'selected))) 1220 (with-current-buffer diff-buf 1221 (cond ((and (equal magit-buffer-range "HEAD^") 1222 (equal magit-buffer-typearg "--cached")) 1223 (magit-diff-staged nil args)) 1224 ((and (equal magit-buffer-range nil) 1225 (equal magit-buffer-typearg "--cached")) 1226 (magit-diff-while-amending args)) 1227 ((magit-anything-staged-p) 1228 (magit-diff-staged nil args)) 1229 (t 1230 (magit-diff-while-amending args)))) 1231 (if (magit-anything-staged-p) 1232 (magit-diff-staged nil args) 1233 (magit-diff-while-amending args))))) 1234 1235 (define-key git-commit-mode-map 1236 (kbd "C-c C-d") 'magit-diff-while-committing) 1237 1238 (defun magit-diff-while-amending (&optional args) 1239 (magit-diff-setup-buffer "HEAD^" "--cached" args nil)) 1240 1241 ;;;###autoload 1242 (defun magit-diff-buffer-file () 1243 "Show diff for the blob or file visited in the current buffer. 1244 1245 When the buffer visits a blob, then show the respective commit. 1246 When the buffer visits a file, then show the differenced between 1247 `HEAD' and the working tree. In both cases limit the diff to 1248 the file or blob." 1249 (interactive) 1250 (require 'magit) 1251 (if-let ((file (magit-file-relative-name))) 1252 (if magit-buffer-refname 1253 (magit-show-commit magit-buffer-refname 1254 (car (magit-show-commit--arguments)) 1255 (list file)) 1256 (save-buffer) 1257 (let ((line (line-number-at-pos)) 1258 (col (current-column))) 1259 (with-current-buffer 1260 (magit-diff-setup-buffer (or (magit-get-current-branch) "HEAD") 1261 nil 1262 (car (magit-diff-arguments)) 1263 (list file) 1264 magit-diff-buffer-file-locked) 1265 (magit-diff--goto-position file line col)))) 1266 (user-error "Buffer isn't visiting a file"))) 1267 1268 ;;;###autoload 1269 (defun magit-diff-paths (a b) 1270 "Show changes between any two files on disk." 1271 (interactive (list (read-file-name "First file: " nil nil t) 1272 (read-file-name "Second file: " nil nil t))) 1273 (magit-diff-setup-buffer nil "--no-index" 1274 nil (list (magit-convert-filename-for-git 1275 (expand-file-name a)) 1276 (magit-convert-filename-for-git 1277 (expand-file-name b))))) 1278 1279 (defun magit-show-commit--arguments () 1280 (pcase-let ((`(,args ,diff-files) 1281 (magit-diff-arguments 'magit-revision-mode))) 1282 (list args (if (derived-mode-p 'magit-log-mode) 1283 (and (or magit-revision-filter-files-on-follow 1284 (not (member "--follow" magit-buffer-log-args))) 1285 magit-buffer-log-files) 1286 diff-files)))) 1287 1288 ;;;###autoload 1289 (defun magit-show-commit (rev &optional args files module) 1290 "Visit the revision at point in another buffer. 1291 If there is no revision at point or with a prefix argument prompt 1292 for a revision." 1293 (interactive 1294 (pcase-let* ((mcommit (magit-section-value-if 'module-commit)) 1295 (atpoint (or mcommit 1296 (thing-at-point 'git-revision t) 1297 (magit-branch-or-commit-at-point))) 1298 (`(,args ,files) (magit-show-commit--arguments))) 1299 (list (or (and (not current-prefix-arg) atpoint) 1300 (magit-read-branch-or-commit "Show commit" atpoint)) 1301 args 1302 files 1303 (and mcommit 1304 (magit-section-parent-value (magit-current-section)))))) 1305 (require 'magit) 1306 (let ((file (magit-file-relative-name))) 1307 (magit-with-toplevel 1308 (when module 1309 (setq default-directory 1310 (expand-file-name (file-name-as-directory module)))) 1311 (unless (magit-commit-p rev) 1312 (user-error "%s is not a commit" rev)) 1313 (let ((buf (magit-revision-setup-buffer rev args files))) 1314 (when file 1315 (save-buffer) 1316 (let ((line (magit-diff-visit--offset file (list "-R" rev) 1317 (line-number-at-pos))) 1318 (col (current-column))) 1319 (with-current-buffer buf 1320 (magit-diff--goto-position file line col)))))))) 1321 1322 (defun magit-diff--locate-hunk (file line &optional parent) 1323 (when-let ((diff (cl-find-if (lambda (section) 1324 (and (cl-typep section 'magit-file-section) 1325 (equal (oref section value) file))) 1326 (oref (or parent magit-root-section) children)))) 1327 (let (hunk (hunks (oref diff children))) 1328 (cl-block nil 1329 (while (setq hunk (pop hunks)) 1330 (when-let ((range (oref hunk to-range))) 1331 (pcase-let* ((`(,beg ,len) range) 1332 (end (+ beg len))) 1333 (cond ((> beg line) (cl-return (list diff nil))) 1334 ((<= beg line end) (cl-return (list hunk t))) 1335 ((null hunks) (cl-return (list hunk nil))))))))))) 1336 1337 (defun magit-diff--goto-position (file line column &optional parent) 1338 (when-let ((pos (magit-diff--locate-hunk file line parent))) 1339 (pcase-let ((`(,section ,exact) pos)) 1340 (cond ((cl-typep section 'magit-file-section) 1341 (goto-char (oref section start))) 1342 (exact 1343 (goto-char (oref section content)) 1344 (let ((pos (car (oref section to-range)))) 1345 (while (or (< pos line) 1346 (= (char-after) ?-)) 1347 (unless (= (char-after) ?-) 1348 (cl-incf pos)) 1349 (forward-line))) 1350 (forward-char (1+ column))) 1351 (t 1352 (goto-char (oref section start)) 1353 (setq section (oref section parent)))) 1354 (while section 1355 (when (oref section hidden) 1356 (magit-section-show section)) 1357 (setq section (oref section parent)))) 1358 (magit-section-update-highlight) 1359 t)) 1360 1361 ;;;; Setting Commands 1362 1363 (defun magit-diff-switch-range-type () 1364 "Convert diff range type. 1365 Change \"revA..revB\" to \"revA...revB\", or vice versa." 1366 (interactive) 1367 (if (and magit-buffer-range 1368 (derived-mode-p 'magit-diff-mode) 1369 (string-match magit-range-re magit-buffer-range)) 1370 (setq magit-buffer-range 1371 (replace-match (if (string= (match-string 2 magit-buffer-range) "..") 1372 "..." 1373 "..") 1374 t t magit-buffer-range 2)) 1375 (user-error "No range to change")) 1376 (magit-refresh)) 1377 1378 (defun magit-diff-flip-revs () 1379 "Swap revisions in diff range. 1380 Change \"revA..revB\" to \"revB..revA\"." 1381 (interactive) 1382 (if (and magit-buffer-range 1383 (derived-mode-p 'magit-diff-mode) 1384 (string-match magit-range-re magit-buffer-range)) 1385 (progn 1386 (setq magit-buffer-range 1387 (concat (match-string 3 magit-buffer-range) 1388 (match-string 2 magit-buffer-range) 1389 (match-string 1 magit-buffer-range))) 1390 (magit-refresh)) 1391 (user-error "No range to swap"))) 1392 1393 (defun magit-diff-toggle-file-filter () 1394 "Toggle the file restriction of the current buffer's diffs. 1395 If the current buffer's mode is derived from `magit-log-mode', 1396 toggle the file restriction in the repository's revision buffer 1397 instead." 1398 (interactive) 1399 (cl-flet ((toggle () 1400 (if (or magit-buffer-diff-files 1401 magit-buffer-diff-files-suspended) 1402 (cl-rotatef magit-buffer-diff-files 1403 magit-buffer-diff-files-suspended) 1404 (setq magit-buffer-diff-files 1405 (transient-infix-read 'magit:--))) 1406 (magit-refresh))) 1407 (cond 1408 ((derived-mode-p 'magit-log-mode 1409 'magit-cherry-mode 1410 'magit-reflog-mode) 1411 (if-let ((buffer (magit-get-mode-buffer 'magit-revision-mode))) 1412 (with-current-buffer buffer (toggle)) 1413 (message "No revision buffer"))) 1414 ((local-variable-p 'magit-buffer-diff-files) 1415 (toggle)) 1416 (t 1417 (user-error "Cannot toggle file filter in this buffer"))))) 1418 1419 (defun magit-diff-less-context (&optional count) 1420 "Decrease the context for diff hunks by COUNT lines." 1421 (interactive "p") 1422 (magit-diff-set-context (lambda (cur) (max 0 (- (or cur 0) count))))) 1423 1424 (defun magit-diff-more-context (&optional count) 1425 "Increase the context for diff hunks by COUNT lines." 1426 (interactive "p") 1427 (magit-diff-set-context (lambda (cur) (+ (or cur 0) count)))) 1428 1429 (defun magit-diff-default-context () 1430 "Reset context for diff hunks to the default height." 1431 (interactive) 1432 (magit-diff-set-context #'ignore)) 1433 1434 (defun magit-diff-set-context (fn) 1435 (let* ((def (--if-let (magit-get "diff.context") (string-to-number it) 3)) 1436 (val magit-buffer-diff-args) 1437 (arg (--first (string-match "^-U\\([0-9]+\\)?$" it) val)) 1438 (num (--if-let (and arg (match-string 1 arg)) (string-to-number it) def)) 1439 (val (delete arg val)) 1440 (num (funcall fn num)) 1441 (arg (and num (not (= num def)) (format "-U%i" num))) 1442 (val (if arg (cons arg val) val))) 1443 (setq magit-buffer-diff-args val)) 1444 (magit-refresh)) 1445 1446 (defun magit-diff-context-p () 1447 (if-let ((arg (--first (string-match "^-U\\([0-9]+\\)$" it) 1448 magit-buffer-diff-args))) 1449 (not (equal arg "-U0")) 1450 t)) 1451 1452 (defun magit-diff-ignore-any-space-p () 1453 (--any-p (member it magit-buffer-diff-args) 1454 '("--ignore-cr-at-eol" 1455 "--ignore-space-at-eol" 1456 "--ignore-space-change" "-b" 1457 "--ignore-all-space" "-w" 1458 "--ignore-blank-space"))) 1459 1460 (defun magit-diff-toggle-refine-hunk (&optional style) 1461 "Turn diff-hunk refining on or off. 1462 1463 If hunk refining is currently on, then hunk refining is turned off. 1464 If hunk refining is off, then hunk refining is turned on, in 1465 `selected' mode (only the currently selected hunk is refined). 1466 1467 With a prefix argument, the \"third choice\" is used instead: 1468 If hunk refining is currently on, then refining is kept on, but 1469 the refining mode (`selected' or `all') is switched. 1470 If hunk refining is off, then hunk refining is turned on, in 1471 `all' mode (all hunks refined). 1472 1473 Customize variable `magit-diff-refine-hunk' to change the default mode." 1474 (interactive "P") 1475 (setq-local magit-diff-refine-hunk 1476 (if style 1477 (if (eq magit-diff-refine-hunk 'all) t 'all) 1478 (not magit-diff-refine-hunk))) 1479 (magit-diff-update-hunk-refinement)) 1480 1481 ;;;; Visit Commands 1482 ;;;;; Dwim Variants 1483 1484 (defun magit-diff-visit-file (file &optional other-window) 1485 "From a diff visit the appropriate version of FILE. 1486 1487 Display the buffer in the selected window. With a prefix 1488 argument OTHER-WINDOW display the buffer in another window 1489 instead. 1490 1491 Visit the worktree version of the appropriate file. The location 1492 of point inside the diff determines which file is being visited. 1493 The visited version depends on what changes the diff is about. 1494 1495 1. If the diff shows uncommitted changes (i.e. stage or unstaged 1496 changes), then visit the file in the working tree (i.e. the 1497 same \"real\" file that `find-file' would visit. In all other 1498 cases visit a \"blob\" (i.e. the version of a file as stored 1499 in some commit). 1500 1501 2. If point is on a removed line, then visit the blob for the 1502 first parent of the commit that removed that line, i.e. the 1503 last commit where that line still exists. 1504 1505 3. If point is on an added or context line, then visit the blob 1506 that adds that line, or if the diff shows from more than a 1507 single commit, then visit the blob from the last of these 1508 commits. 1509 1510 In the file-visiting buffer also go to the line that corresponds 1511 to the line that point is on in the diff. 1512 1513 Note that this command only works if point is inside a diff. 1514 In other cases `magit-find-file' (which see) has to be used." 1515 (interactive (list (magit-file-at-point t t) current-prefix-arg)) 1516 (magit-diff-visit-file--internal file nil 1517 (if other-window 1518 #'switch-to-buffer-other-window 1519 #'pop-to-buffer-same-window))) 1520 1521 (defun magit-diff-visit-file-other-window (file) 1522 "From a diff visit the appropriate version of FILE in another window. 1523 Like `magit-diff-visit-file' but use 1524 `switch-to-buffer-other-window'." 1525 (interactive (list (magit-file-at-point t t))) 1526 (magit-diff-visit-file--internal file nil #'switch-to-buffer-other-window)) 1527 1528 (defun magit-diff-visit-file-other-frame (file) 1529 "From a diff visit the appropriate version of FILE in another frame. 1530 Like `magit-diff-visit-file' but use 1531 `switch-to-buffer-other-frame'." 1532 (interactive (list (magit-file-at-point t t))) 1533 (magit-diff-visit-file--internal file nil #'switch-to-buffer-other-frame)) 1534 1535 ;;;;; Worktree Variants 1536 1537 (defun magit-diff-visit-worktree-file (file &optional other-window) 1538 "From a diff visit the worktree version of FILE. 1539 1540 Display the buffer in the selected window. With a prefix 1541 argument OTHER-WINDOW display the buffer in another window 1542 instead. 1543 1544 Visit the worktree version of the appropriate file. The location 1545 of point inside the diff determines which file is being visited. 1546 1547 Unlike `magit-diff-visit-file' always visits the \"real\" file in 1548 the working tree, i.e the \"current version\" of the file. 1549 1550 In the file-visiting buffer also go to the line that corresponds 1551 to the line that point is on in the diff. Lines that were added 1552 or removed in the working tree, the index and other commits in 1553 between are automatically accounted for." 1554 (interactive (list (magit-file-at-point t t) current-prefix-arg)) 1555 (magit-diff-visit-file--internal file t 1556 (if other-window 1557 #'switch-to-buffer-other-window 1558 #'pop-to-buffer-same-window))) 1559 1560 (defun magit-diff-visit-worktree-file-other-window (file) 1561 "From a diff visit the worktree version of FILE in another window. 1562 Like `magit-diff-visit-worktree-file' but use 1563 `switch-to-buffer-other-window'." 1564 (interactive (list (magit-file-at-point t t))) 1565 (magit-diff-visit-file--internal file t #'switch-to-buffer-other-window)) 1566 1567 (defun magit-diff-visit-worktree-file-other-frame (file) 1568 "From a diff visit the worktree version of FILE in another frame. 1569 Like `magit-diff-visit-worktree-file' but use 1570 `switch-to-buffer-other-frame'." 1571 (interactive (list (magit-file-at-point t t))) 1572 (magit-diff-visit-file--internal file t #'switch-to-buffer-other-frame)) 1573 1574 ;;;;; Internal 1575 1576 (defun magit-diff-visit-file--internal (file force-worktree fn) 1577 "From a diff visit the appropriate version of FILE. 1578 If FORCE-WORKTREE is non-nil, then visit the worktree version of 1579 the file, even if the diff is about a committed change. Use FN 1580 to display the buffer in some window." 1581 (if (magit-file-accessible-directory-p file) 1582 (magit-diff-visit-directory file force-worktree) 1583 (pcase-let ((`(,buf ,pos) 1584 (magit-diff-visit-file--noselect file force-worktree))) 1585 (funcall fn buf) 1586 (magit-diff-visit-file--setup buf pos) 1587 buf))) 1588 1589 (defun magit-diff-visit-directory (directory &optional other-window) 1590 "Visit DIRECTORY in some window. 1591 Display the buffer in the selected window unless OTHER-WINDOW is 1592 non-nil. If DIRECTORY is the top-level directory of the current 1593 repository, then visit the containing directory using Dired and 1594 in the Dired buffer put point on DIRECTORY. Otherwise display 1595 the Magit-Status buffer for DIRECTORY." 1596 (if (equal (magit-toplevel directory) 1597 (magit-toplevel)) 1598 (dired-jump other-window (concat directory "/.")) 1599 (let ((display-buffer-overriding-action 1600 (if other-window 1601 '(nil (inhibit-same-window t)) 1602 '(display-buffer-same-window)))) 1603 (magit-status-setup-buffer directory)))) 1604 1605 (defun magit-diff-visit-file--setup (buf pos) 1606 (if-let ((win (get-buffer-window buf 'visible))) 1607 (with-selected-window win 1608 (when pos 1609 (unless (<= (point-min) pos (point-max)) 1610 (widen)) 1611 (goto-char pos)) 1612 (when (and buffer-file-name 1613 (magit-anything-unmerged-p buffer-file-name)) 1614 (smerge-start-session)) 1615 (run-hooks 'magit-diff-visit-file-hook)) 1616 (error "File buffer is not visible"))) 1617 1618 (defun magit-diff-visit-file--noselect (&optional file goto-worktree) 1619 (unless file 1620 (setq file (magit-file-at-point t t))) 1621 (let* ((hunk (magit-diff-visit--hunk)) 1622 (goto-from (and hunk 1623 (magit-diff-visit--goto-from-p hunk goto-worktree))) 1624 (line (and hunk (magit-diff-hunk-line hunk goto-from))) 1625 (col (and hunk (magit-diff-hunk-column hunk goto-from))) 1626 (spec (magit-diff--dwim)) 1627 (rev (if goto-from 1628 (magit-diff-visit--range-from spec) 1629 (magit-diff-visit--range-to spec))) 1630 (buf (if (or goto-worktree 1631 (and (not (stringp rev)) 1632 (or magit-diff-visit-avoid-head-blob 1633 (not goto-from)))) 1634 (or (get-file-buffer file) 1635 (find-file-noselect file)) 1636 (magit-find-file-noselect (if (stringp rev) rev "HEAD") 1637 file)))) 1638 (if line 1639 (with-current-buffer buf 1640 (cond ((eq rev 'staged) 1641 (setq line (magit-diff-visit--offset file nil line))) 1642 ((and goto-worktree 1643 (stringp rev)) 1644 (setq line (magit-diff-visit--offset file rev line)))) 1645 (list buf (save-restriction 1646 (widen) 1647 (goto-char (point-min)) 1648 (forward-line (1- line)) 1649 (move-to-column col) 1650 (point)))) 1651 (list buf nil)))) 1652 1653 (defun magit-diff-visit--hunk () 1654 (when-let ((scope (magit-diff-scope))) 1655 (let ((section (magit-current-section))) 1656 (cl-case scope 1657 ((file files) 1658 (setq section (car (oref section children)))) 1659 (list 1660 (setq section (car (oref section children))) 1661 (when section 1662 (setq section (car (oref section children)))))) 1663 (and 1664 ;; Unmerged files appear in the list of staged changes 1665 ;; but unlike in the list of unstaged changes no diffs 1666 ;; are shown here. In that case `section' is nil. 1667 section 1668 ;; Currently the `hunk' type is also abused for file 1669 ;; mode changes, which we are not interested in here. 1670 ;; Such sections have no value. 1671 (oref section value) 1672 section)))) 1673 1674 (defun magit-diff-visit--goto-from-p (section in-worktree) 1675 (and magit-diff-visit-previous-blob 1676 (not in-worktree) 1677 (not (oref section combined)) 1678 (not (< (point) (oref section content))) 1679 (= (char-after (line-beginning-position)) ?-))) 1680 1681 (defvar magit-diff-visit-jump-to-change t) 1682 1683 (defun magit-diff-hunk-line (section goto-from) 1684 (save-excursion 1685 (goto-char (line-beginning-position)) 1686 (with-slots (content combined from-ranges from-range to-range) section 1687 (when (and magit-diff-visit-jump-to-change (< (point) content)) 1688 (goto-char content) 1689 (re-search-forward "^[-+]")) 1690 (+ (car (if goto-from from-range to-range)) 1691 (let ((prefix (if combined (length from-ranges) 1)) 1692 (target (point)) 1693 (offset 0)) 1694 (goto-char content) 1695 (while (< (point) target) 1696 (unless (string-match-p 1697 (if goto-from "\\+" "-") 1698 (buffer-substring (point) (+ (point) prefix))) 1699 (cl-incf offset)) 1700 (forward-line)) 1701 offset))))) 1702 1703 (defun magit-diff-hunk-column (section goto-from) 1704 (if (or (< (point) 1705 (oref section content)) 1706 (and (not goto-from) 1707 (= (char-after (line-beginning-position)) ?-))) 1708 0 1709 (max 0 (- (+ (current-column) 2) 1710 (length (oref section value)))))) 1711 1712 (defun magit-diff-visit--range-from (spec) 1713 (cond ((consp spec) 1714 (concat (cdr spec) "^")) 1715 ((stringp spec) 1716 (car (magit-split-range spec))) 1717 (t 1718 spec))) 1719 1720 (defun magit-diff-visit--range-to (spec) 1721 (if (symbolp spec) 1722 spec 1723 (let ((rev (if (consp spec) 1724 (cdr spec) 1725 (cdr (magit-split-range spec))))) 1726 (if (and magit-diff-visit-avoid-head-blob 1727 (magit-rev-head-p rev)) 1728 'unstaged 1729 rev)))) 1730 1731 (defun magit-diff-visit--offset (file rev line) 1732 (let ((offset 0)) 1733 (with-temp-buffer 1734 (save-excursion 1735 (magit-with-toplevel 1736 (magit-git-insert "diff" rev "--" file))) 1737 (catch 'found 1738 (while (re-search-forward 1739 "^@@ -\\([0-9]+\\),\\([0-9]+\\) \\+\\([0-9]+\\),\\([0-9]+\\) @@.*\n" 1740 nil t) 1741 (let ((from-beg (string-to-number (match-string 1))) 1742 (from-len (string-to-number (match-string 2))) 1743 ( to-len (string-to-number (match-string 4)))) 1744 (if (<= from-beg line) 1745 (if (< (+ from-beg from-len) line) 1746 (cl-incf offset (- to-len from-len)) 1747 (let ((rest (- line from-beg))) 1748 (while (> rest 0) 1749 (pcase (char-after) 1750 (?\s (cl-decf rest)) 1751 (?- (cl-decf offset) (cl-decf rest)) 1752 (?+ (cl-incf offset))) 1753 (forward-line)))) 1754 (throw 'found nil)))))) 1755 (+ line offset))) 1756 1757 ;;;; Scroll Commands 1758 1759 (defun magit-diff-show-or-scroll-up () 1760 "Update the commit or diff buffer for the thing at point. 1761 1762 Either show the commit or stash at point in the appropriate 1763 buffer, or if that buffer is already being displayed in the 1764 current frame and contains information about that commit or 1765 stash, then instead scroll the buffer up. If there is no 1766 commit or stash at point, then prompt for a commit." 1767 (interactive) 1768 (magit-diff-show-or-scroll 'scroll-up)) 1769 1770 (defun magit-diff-show-or-scroll-down () 1771 "Update the commit or diff buffer for the thing at point. 1772 1773 Either show the commit or stash at point in the appropriate 1774 buffer, or if that buffer is already being displayed in the 1775 current frame and contains information about that commit or 1776 stash, then instead scroll the buffer down. If there is no 1777 commit or stash at point, then prompt for a commit." 1778 (interactive) 1779 (magit-diff-show-or-scroll 'scroll-down)) 1780 1781 (defun magit-diff-show-or-scroll (fn) 1782 (let (rev cmd buf win) 1783 (cond 1784 (magit-blame-mode 1785 (setq rev (oref (magit-current-blame-chunk) orig-rev)) 1786 (setq cmd 'magit-show-commit) 1787 (setq buf (magit-get-mode-buffer 'magit-revision-mode))) 1788 ((derived-mode-p 'git-rebase-mode) 1789 (with-slots (action-type target) 1790 (git-rebase-current-line) 1791 (if (not (eq action-type 'commit)) 1792 (user-error "No commit on this line") 1793 (setq rev target) 1794 (setq cmd 'magit-show-commit) 1795 (setq buf (magit-get-mode-buffer 'magit-revision-mode))))) 1796 (t 1797 (magit-section-case 1798 (branch 1799 (setq rev (magit-ref-maybe-qualify (oref it value))) 1800 (setq cmd 'magit-show-commit) 1801 (setq buf (magit-get-mode-buffer 'magit-revision-mode))) 1802 (commit 1803 (setq rev (oref it value)) 1804 (setq cmd 'magit-show-commit) 1805 (setq buf (magit-get-mode-buffer 'magit-revision-mode))) 1806 (stash 1807 (setq rev (oref it value)) 1808 (setq cmd 'magit-stash-show) 1809 (setq buf (magit-get-mode-buffer 'magit-stash-mode)))))) 1810 (if rev 1811 (if (and buf 1812 (setq win (get-buffer-window buf)) 1813 (with-current-buffer buf 1814 (and (equal rev magit-buffer-revision) 1815 (equal (magit-rev-parse rev) 1816 magit-buffer-revision-hash)))) 1817 (with-selected-window win 1818 (condition-case nil 1819 (funcall fn) 1820 (error 1821 (goto-char (pcase fn 1822 (`scroll-up (point-min)) 1823 (`scroll-down (point-max))))))) 1824 (let ((magit-display-buffer-noselect t)) 1825 (if (eq cmd 'magit-show-commit) 1826 (apply #'magit-show-commit rev (magit-show-commit--arguments)) 1827 (funcall cmd rev)))) 1828 (call-interactively #'magit-show-commit)))) 1829 1830 ;;;; Section Commands 1831 1832 (defun magit-section-cycle-diffs () 1833 "Cycle visibility of diff-related sections in the current buffer." 1834 (interactive) 1835 (when-let ((sections 1836 (cond ((derived-mode-p 'magit-status-mode) 1837 (--mapcat 1838 (when it 1839 (when (oref it hidden) 1840 (magit-section-show it)) 1841 (oref it children)) 1842 (list (magit-get-section '((staged) (status))) 1843 (magit-get-section '((unstaged) (status)))))) 1844 ((derived-mode-p 'magit-diff-mode) 1845 (-filter #'magit-file-section-p 1846 (oref magit-root-section children)))))) 1847 (if (--any-p (oref it hidden) sections) 1848 (dolist (s sections) 1849 (magit-section-show s) 1850 (magit-section-hide-children s)) 1851 (let ((children (--mapcat (oref it children) sections))) 1852 (cond ((and (--any-p (oref it hidden) children) 1853 (--any-p (oref it children) children)) 1854 (mapc 'magit-section-show-headings sections)) 1855 ((seq-some 'magit-section-hidden-body children) 1856 (mapc 'magit-section-show-children sections)) 1857 (t 1858 (mapc 'magit-section-hide sections))))))) 1859 1860 ;;; Diff Mode 1861 1862 (defvar magit-diff-mode-map 1863 (let ((map (make-sparse-keymap))) 1864 (set-keymap-parent map magit-mode-map) 1865 (define-key map (kbd "C-c C-d") 'magit-diff-while-committing) 1866 (define-key map (kbd "C-c C-b") 'magit-go-backward) 1867 (define-key map (kbd "C-c C-f") 'magit-go-forward) 1868 (define-key map (kbd "SPC") 'scroll-up) 1869 (define-key map (kbd "DEL") 'scroll-down) 1870 (define-key map (kbd "j") 'magit-jump-to-diffstat-or-diff) 1871 (define-key map [remap write-file] 'magit-patch-save) 1872 map) 1873 "Keymap for `magit-diff-mode'.") 1874 1875 (define-derived-mode magit-diff-mode magit-mode "Magit Diff" 1876 "Mode for looking at a Git diff. 1877 1878 This mode is documented in info node `(magit)Diff Buffer'. 1879 1880 \\<magit-mode-map>\ 1881 Type \\[magit-refresh] to refresh the current buffer. 1882 Type \\[magit-section-toggle] to expand or hide the section at point. 1883 Type \\[magit-visit-thing] to visit the hunk or file at point. 1884 1885 Staging and applying changes is documented in info node 1886 `(magit)Staging and Unstaging' and info node `(magit)Applying'. 1887 1888 \\<magit-hunk-section-map>Type \ 1889 \\[magit-apply] to apply the change at point, \ 1890 \\[magit-stage] to stage, 1891 \\[magit-unstage] to unstage, \ 1892 \\[magit-discard] to discard, or \ 1893 \\[magit-reverse] to reverse it. 1894 1895 \\{magit-diff-mode-map}" 1896 :group 'magit-diff 1897 (hack-dir-local-variables-non-file-buffer) 1898 (setq imenu-prev-index-position-function 1899 'magit-imenu--diff-prev-index-position-function) 1900 (setq imenu-extract-index-name-function 1901 'magit-imenu--diff-extract-index-name-function)) 1902 1903 (put 'magit-diff-mode 'magit-diff-default-arguments 1904 '("--stat" "--no-ext-diff")) 1905 1906 (defun magit-diff-setup-buffer (range typearg args files &optional locked) 1907 (require 'magit) 1908 (magit-setup-buffer #'magit-diff-mode locked 1909 (magit-buffer-range range) 1910 (magit-buffer-typearg typearg) 1911 (magit-buffer-diff-args args) 1912 (magit-buffer-diff-files files) 1913 (magit-buffer-diff-files-suspended nil))) 1914 1915 (defun magit-diff-refresh-buffer () 1916 "Refresh the current `magit-diff-mode' buffer." 1917 (magit-set-header-line-format 1918 (if (equal magit-buffer-typearg "--no-index") 1919 (apply #'format "Differences between %s and %s" magit-buffer-diff-files) 1920 (concat (if magit-buffer-range 1921 (cond 1922 ((string-match-p "\\(\\.\\.\\|\\^-\\)" 1923 magit-buffer-range) 1924 (format "Changes in %s" magit-buffer-range)) 1925 ((member "-R" magit-buffer-diff-args) 1926 (format "Changes from working tree to %s" magit-buffer-range)) 1927 (t 1928 (format "Changes from %s to working tree" magit-buffer-range))) 1929 (if (equal magit-buffer-typearg "--cached") 1930 "Staged changes" 1931 "Unstaged changes")) 1932 (pcase (length magit-buffer-diff-files) 1933 (0) 1934 (1 (concat " in file " (car magit-buffer-diff-files))) 1935 (_ (concat " in files " 1936 (mapconcat #'identity magit-buffer-diff-files ", "))))))) 1937 (setq magit-buffer-range-hashed 1938 (and magit-buffer-range (magit-hash-range magit-buffer-range))) 1939 (magit-insert-section (diffbuf) 1940 (magit-run-section-hook 'magit-diff-sections-hook))) 1941 1942 (cl-defmethod magit-buffer-value (&context (major-mode magit-diff-mode)) 1943 (nconc (cond (magit-buffer-range 1944 (delq nil (list magit-buffer-range magit-buffer-typearg))) 1945 ((equal magit-buffer-typearg "--cached") 1946 (list 'staged)) 1947 (t 1948 (list 'unstaged magit-buffer-typearg))) 1949 (and magit-buffer-diff-files (cons "--" magit-buffer-diff-files)))) 1950 1951 (defvar magit-diff-section-base-map 1952 (let ((map (make-sparse-keymap))) 1953 (define-key map (kbd "C-j") 'magit-diff-visit-worktree-file) 1954 (define-key map (kbd "C-<return>") 'magit-diff-visit-worktree-file) 1955 (define-key map (kbd "C-x 4 <return>") 'magit-diff-visit-file-other-window) 1956 (define-key map (kbd "C-x 5 <return>") 'magit-diff-visit-file-other-frame) 1957 (define-key map [remap magit-visit-thing] 'magit-diff-visit-file) 1958 (define-key map [remap magit-delete-thing] 'magit-discard) 1959 (define-key map [remap magit-revert-no-commit] 'magit-reverse) 1960 (define-key map "a" 'magit-apply) 1961 (define-key map "s" 'magit-stage) 1962 (define-key map "u" 'magit-unstage) 1963 (define-key map "&" 'magit-do-async-shell-command) 1964 (define-key map "C" 'magit-commit-add-log) 1965 (define-key map (kbd "C-x a") 'magit-add-change-log-entry) 1966 (define-key map (kbd "C-x 4 a") 'magit-add-change-log-entry-other-window) 1967 (define-key map (kbd "C-c C-t") 'magit-diff-trace-definition) 1968 (define-key map (kbd "C-c C-e") 'magit-diff-edit-hunk-commit) 1969 map) 1970 "Parent of `magit-{hunk,file}-section-map'.") 1971 1972 (defvar magit-file-section-map 1973 (let ((map (make-sparse-keymap))) 1974 (set-keymap-parent map magit-diff-section-base-map) 1975 map) 1976 "Keymap for `file' sections.") 1977 1978 (defvar magit-hunk-section-map 1979 (let ((map (make-sparse-keymap))) 1980 (set-keymap-parent map magit-diff-section-base-map) 1981 (let ((m (make-sparse-keymap))) 1982 (define-key m (kbd "RET") 'magit-smerge-keep-current) 1983 (define-key m (kbd "u") 'magit-smerge-keep-upper) 1984 (define-key m (kbd "b") 'magit-smerge-keep-base) 1985 (define-key m (kbd "l") 'magit-smerge-keep-lower) 1986 (define-key map smerge-command-prefix m)) 1987 map) 1988 "Keymap for `hunk' sections.") 1989 1990 (defconst magit-diff-conflict-headline-re 1991 (concat "^" (regexp-opt 1992 ;; Defined in merge-tree.c in this order. 1993 '("merged" 1994 "added in remote" 1995 "added in both" 1996 "added in local" 1997 "removed in both" 1998 "changed in both" 1999 "removed in local" 2000 "removed in remote")))) 2001 2002 (defconst magit-diff-headline-re 2003 (concat "^\\(@@@?\\|diff\\|Submodule\\|" 2004 "\\* Unmerged path\\|" 2005 (substring magit-diff-conflict-headline-re 1) 2006 "\\)")) 2007 2008 (defconst magit-diff-statline-re 2009 (concat "^ ?" 2010 "\\(.*\\)" ; file 2011 "\\( +| +\\)" ; separator 2012 "\\([0-9]+\\|Bin\\(?: +[0-9]+ -> [0-9]+ bytes\\)?$\\) ?" 2013 "\\(\\+*\\)" ; add 2014 "\\(-*\\)$")) ; del 2015 2016 (defvar magit-diff--reset-non-color-moved 2017 (list 2018 "-c" "color.diff.context=normal" 2019 "-c" "color.diff.plain=normal" ; historical synonym for context 2020 "-c" "color.diff.meta=normal" 2021 "-c" "color.diff.frag=normal" 2022 "-c" "color.diff.func=normal" 2023 "-c" "color.diff.old=normal" 2024 "-c" "color.diff.new=normal" 2025 "-c" "color.diff.commit=normal" 2026 "-c" "color.diff.whitespace=normal" 2027 ;; "git-range-diff" does not support "--color-moved", so we don't 2028 ;; need to reset contextDimmed, oldDimmed, newDimmed, contextBold, 2029 ;; oldBold, and newBold. 2030 )) 2031 2032 (defun magit-insert-diff () 2033 "Insert the diff into this `magit-diff-mode' buffer." 2034 (magit--insert-diff 2035 "diff" magit-buffer-range "-p" "--no-prefix" 2036 (and (member "--stat" magit-buffer-diff-args) "--numstat") 2037 magit-buffer-typearg 2038 magit-buffer-diff-args "--" 2039 magit-buffer-diff-files)) 2040 2041 (defun magit--insert-diff (&rest args) 2042 (declare (indent 0)) 2043 (pcase-let ((`(,cmd . ,args) 2044 (-flatten args)) 2045 (magit-git-global-arguments 2046 (remove "--literal-pathspecs" magit-git-global-arguments))) 2047 ;; As of Git 2.19.0, we need to generate diffs with 2048 ;; --ita-visible-in-index so that `magit-stage' can work with 2049 ;; intent-to-add files (see #4026). Cache the result for each 2050 ;; repo to avoid a `git version' call for every diff insertion. 2051 (when (and (not (equal cmd "merge-tree")) 2052 (pcase (magit-repository-local-get 'diff-ita-kludge-p 'unset) 2053 (`unset 2054 (let ((val (version<= "2.19.0" (magit-git-version)))) 2055 (magit-repository-local-set 'diff-ita-kludge-p val) 2056 val)) 2057 (val val))) 2058 (push "--ita-visible-in-index" args)) 2059 (setq args (magit-diff--maybe-add-stat-arguments args)) 2060 (when (cl-member-if (lambda (arg) (string-prefix-p "--color-moved" arg)) args) 2061 (push "--color=always" args) 2062 (setq magit-git-global-arguments 2063 (append magit-diff--reset-non-color-moved 2064 magit-git-global-arguments))) 2065 (magit-git-wash #'magit-diff-wash-diffs cmd args))) 2066 2067 (defun magit-diff--maybe-add-stat-arguments (args) 2068 (if (member "--stat" args) 2069 (append (if (functionp magit-diff-extra-stat-arguments) 2070 (funcall magit-diff-extra-stat-arguments) 2071 magit-diff-extra-stat-arguments) 2072 args) 2073 args)) 2074 2075 (defun magit-diff-use-window-width-as-stat-width () 2076 "Use the `window-width' as the value of `--stat-width'." 2077 (when-let ((window (get-buffer-window (current-buffer) 'visible))) 2078 (list (format "--stat-width=%d" (window-width window))))) 2079 2080 (defun magit-diff-wash-diffs (args &optional limit) 2081 (run-hooks 'magit-diff-wash-diffs-hook) 2082 (when (member "--show-signature" args) 2083 (magit-diff-wash-signature)) 2084 (when (member "--stat" args) 2085 (magit-diff-wash-diffstat)) 2086 (when (re-search-forward magit-diff-headline-re limit t) 2087 (goto-char (line-beginning-position)) 2088 (magit-wash-sequence (apply-partially 'magit-diff-wash-diff args)) 2089 (insert ?\n))) 2090 2091 (defun magit-jump-to-diffstat-or-diff () 2092 "Jump to the diffstat or diff. 2093 When point is on a file inside the diffstat section, then jump 2094 to the respective diff section, otherwise jump to the diffstat 2095 section or a child thereof." 2096 (interactive) 2097 (--if-let (magit-get-section 2098 (append (magit-section-case 2099 ([file diffstat] `((file . ,(oref it value)))) 2100 (file `((file . ,(oref it value)) (diffstat))) 2101 (t '((diffstat)))) 2102 (magit-section-ident magit-root-section))) 2103 (magit-section-goto it) 2104 (user-error "No diffstat in this buffer"))) 2105 2106 (defun magit-diff-wash-signature () 2107 (when (looking-at "^gpg: ") 2108 (let (title end) 2109 (save-excursion 2110 (while (looking-at "^gpg: ") 2111 (cond 2112 ((looking-at "^gpg: Good signature from") 2113 (setq title (propertize 2114 (buffer-substring (point) (line-end-position)) 2115 'face 'magit-signature-good))) 2116 ((looking-at "^gpg: Can't check signature") 2117 (setq title (propertize 2118 (buffer-substring (point) (line-end-position)) 2119 'face '(italic bold))))) 2120 (forward-line)) 2121 (setq end (point-marker))) 2122 (magit-insert-section (signature magit-buffer-revision title) 2123 (when title 2124 (magit-insert-heading title)) 2125 (goto-char end) 2126 (insert "\n"))))) 2127 2128 (defun magit-diff-wash-diffstat () 2129 (let (heading (beg (point))) 2130 (when (re-search-forward "^ ?\\([0-9]+ +files? change[^\n]*\n\\)" nil t) 2131 (setq heading (match-string 1)) 2132 (magit-delete-match) 2133 (goto-char beg) 2134 (magit-insert-section (diffstat) 2135 (insert (propertize heading 'font-lock-face 'magit-diff-file-heading)) 2136 (magit-insert-heading) 2137 (let (files) 2138 (while (looking-at "^[-0-9]+\t[-0-9]+\t\\(.+\\)$") 2139 (push (magit-decode-git-path 2140 (let ((f (match-string 1))) 2141 (cond 2142 ((string-match "\\`\\([^{]+\\){\\(.+\\) => \\(.+\\)}\\'" f) 2143 (concat (match-string 1 f) 2144 (match-string 3 f))) 2145 ((string-match " => " f) 2146 (substring f (match-end 0))) 2147 (t f)))) 2148 files) 2149 (magit-delete-line)) 2150 (setq files (nreverse files)) 2151 (while (looking-at magit-diff-statline-re) 2152 (magit-bind-match-strings (file sep cnt add del) nil 2153 (magit-delete-line) 2154 (when (string-match " +$" file) 2155 (setq sep (concat (match-string 0 file) sep)) 2156 (setq file (substring file 0 (match-beginning 0)))) 2157 (let ((le (length file)) ld) 2158 (setq file (magit-decode-git-path file)) 2159 (setq ld (length file)) 2160 (when (> le ld) 2161 (setq sep (concat (make-string (- le ld) ?\s) sep)))) 2162 (magit-insert-section (file (pop files)) 2163 (insert (propertize file 'font-lock-face 'magit-filename) 2164 sep cnt " ") 2165 (when add 2166 (insert (propertize add 'font-lock-face 2167 'magit-diffstat-added))) 2168 (when del 2169 (insert (propertize del 'font-lock-face 2170 'magit-diffstat-removed))) 2171 (insert "\n"))))) 2172 (if (looking-at "^$") (forward-line) (insert "\n")))))) 2173 2174 (defun magit-diff-wash-diff (args) 2175 (when (cl-member-if (lambda (arg) (string-prefix-p "--color-moved" arg)) args) 2176 (require 'ansi-color) 2177 (ansi-color-apply-on-region (point-min) (point-max))) 2178 (cond 2179 ((looking-at "^Submodule") 2180 (magit-diff-wash-submodule)) 2181 ((looking-at "^\\* Unmerged path \\(.*\\)") 2182 (let ((file (magit-decode-git-path (match-string 1)))) 2183 (magit-delete-line) 2184 (unless (and (derived-mode-p 'magit-status-mode) 2185 (not (member "--cached" args))) 2186 (magit-insert-section (file file) 2187 (insert (propertize 2188 (format "unmerged %s%s" file 2189 (pcase (cddr (car (magit-file-status file))) 2190 (`(?D ?D) " (both deleted)") 2191 (`(?D ?U) " (deleted by us)") 2192 (`(?U ?D) " (deleted by them)") 2193 (`(?A ?A) " (both added)") 2194 (`(?A ?U) " (added by us)") 2195 (`(?U ?A) " (added by them)") 2196 (`(?U ?U) ""))) 2197 'font-lock-face 'magit-diff-file-heading)) 2198 (insert ?\n)))) 2199 t) 2200 ((looking-at magit-diff-conflict-headline-re) 2201 (let ((long-status (match-string 0)) 2202 (status "BUG") 2203 file orig base) 2204 (if (equal long-status "merged") 2205 (progn (setq status long-status) 2206 (setq long-status nil)) 2207 (setq status (pcase-exhaustive long-status 2208 ("added in remote" "new file") 2209 ("added in both" "new file") 2210 ("added in local" "new file") 2211 ("removed in both" "removed") 2212 ("changed in both" "changed") 2213 ("removed in local" "removed") 2214 ("removed in remote" "removed")))) 2215 (magit-delete-line) 2216 (while (looking-at 2217 "^ \\([^ ]+\\) +[0-9]\\{6\\} \\([a-z0-9]\\{40\\}\\) \\(.+\\)$") 2218 (magit-bind-match-strings (side _blob name) nil 2219 (pcase side 2220 ("result" (setq file name)) 2221 ("our" (setq orig name)) 2222 ("their" (setq file name)) 2223 ("base" (setq base name)))) 2224 (magit-delete-line)) 2225 (when orig (setq orig (magit-decode-git-path orig))) 2226 (when file (setq file (magit-decode-git-path file))) 2227 (magit-diff-insert-file-section 2228 (or file base) orig status nil nil nil long-status))) 2229 ;; The files on this line may be ambiguous due to whitespace. 2230 ;; That's okay. We can get their names from subsequent headers. 2231 ((looking-at "^diff --\ 2232 \\(?:\\(?1:git\\) \\(?:\\(?2:.+?\\) \\2\\)?\ 2233 \\|\\(?:cc\\|combined\\) \\(?3:.+\\)\\)") 2234 (let ((status (cond ((equal (match-string 1) "git") "modified") 2235 ((derived-mode-p 'magit-revision-mode) "resolved") 2236 (t "unmerged"))) 2237 (orig nil) 2238 (file (or (match-string 2) (match-string 3))) 2239 (header (list (buffer-substring-no-properties 2240 (line-beginning-position) (1+ (line-end-position))))) 2241 (modes nil) 2242 (rename nil)) 2243 (magit-delete-line) 2244 (while (not (or (eobp) (looking-at magit-diff-headline-re))) 2245 (cond 2246 ((looking-at "old mode \\(?:[^\n]+\\)\nnew mode \\(?:[^\n]+\\)\n") 2247 (setq modes (match-string 0))) 2248 ((looking-at "deleted file .+\n") 2249 (setq status "deleted")) 2250 ((looking-at "new file .+\n") 2251 (setq status "new file")) 2252 ((looking-at "rename from \\(.+\\)\nrename to \\(.+\\)\n") 2253 (setq rename (match-string 0)) 2254 (setq orig (match-string 1)) 2255 (setq file (match-string 2)) 2256 (setq status "renamed")) 2257 ((looking-at "copy from \\(.+\\)\ncopy to \\(.+\\)\n") 2258 (setq orig (match-string 1)) 2259 (setq file (match-string 2)) 2260 (setq status "new file")) 2261 ((looking-at "similarity index .+\n")) 2262 ((looking-at "dissimilarity index .+\n")) 2263 ((looking-at "index .+\n")) 2264 ((looking-at "--- \\(.+?\\)\t?\n") 2265 (unless (equal (match-string 1) "/dev/null") 2266 (setq orig (match-string 1)))) 2267 ((looking-at "\\+\\+\\+ \\(.+?\\)\t?\n") 2268 (unless (equal (match-string 1) "/dev/null") 2269 (setq file (match-string 1)))) 2270 ((looking-at "Binary files .+ and .+ differ\n")) 2271 ;; TODO Use all combined diff extended headers. 2272 ((looking-at "mode .+\n")) 2273 (t 2274 (error "BUG: Unknown extended header: %S" 2275 (buffer-substring (point) (line-end-position))))) 2276 ;; These headers are treated as some sort of special hunk. 2277 (unless (or (string-prefix-p "old mode" (match-string 0)) 2278 (string-prefix-p "rename" (match-string 0))) 2279 (push (match-string 0) header)) 2280 (magit-delete-match)) 2281 (setq header (mapconcat #'identity (nreverse header) "")) 2282 (when orig 2283 (setq orig (magit-decode-git-path orig))) 2284 (setq file (magit-decode-git-path file)) 2285 ;; KLUDGE `git-diff' ignores `--no-prefix' for new files and renames at 2286 ;; least. And `git-log' ignores `--no-prefix' when `-L' is used. 2287 (when (or (and file orig 2288 (string-prefix-p "a/" orig) 2289 (string-prefix-p "b/" file)) 2290 (and (derived-mode-p 'magit-log-mode) 2291 (--first (string-match-p "\\`-L" it) 2292 magit-buffer-log-args))) 2293 (setq file (substring file 2)) 2294 (when orig 2295 (setq orig (substring orig 2)))) 2296 (magit-diff-insert-file-section file orig status modes rename header))))) 2297 2298 (defun magit-diff-insert-file-section 2299 (file orig status modes rename header &optional long-status) 2300 (magit-insert-section section 2301 (file file (or (equal status "deleted") 2302 (derived-mode-p 'magit-status-mode))) 2303 (insert (propertize (format "%-10s %s" status 2304 (if (or (not orig) (equal orig file)) 2305 file 2306 (format "%s -> %s" orig file))) 2307 'font-lock-face 'magit-diff-file-heading)) 2308 (when long-status 2309 (insert (format " (%s)" long-status))) 2310 (magit-insert-heading) 2311 (unless (equal orig file) 2312 (oset section source orig)) 2313 (oset section header header) 2314 (when modes 2315 (magit-insert-section (hunk '(chmod)) 2316 (insert modes) 2317 (magit-insert-heading))) 2318 (when rename 2319 (magit-insert-section (hunk '(rename)) 2320 (insert rename) 2321 (magit-insert-heading))) 2322 (magit-wash-sequence #'magit-diff-wash-hunk))) 2323 2324 (defun magit-diff-wash-submodule () 2325 ;; See `show_submodule_summary' in submodule.c and "this" commit. 2326 (when (looking-at "^Submodule \\([^ ]+\\)") 2327 (let ((module (match-string 1)) 2328 untracked modified) 2329 (when (looking-at "^Submodule [^ ]+ contains untracked content$") 2330 (magit-delete-line) 2331 (setq untracked t)) 2332 (when (looking-at "^Submodule [^ ]+ contains modified content$") 2333 (magit-delete-line) 2334 (setq modified t)) 2335 (cond 2336 ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ :]+\\)\\( (rewind)\\)?:$") 2337 (equal (match-string 1) module)) 2338 (magit-bind-match-strings (_module range rewind) nil 2339 (magit-delete-line) 2340 (while (looking-at "^ \\([<>]\\) \\(.+\\)$") 2341 (magit-delete-line)) 2342 (when rewind 2343 (setq range (replace-regexp-in-string "[^.]\\(\\.\\.\\)[^.]" 2344 "..." range t t 1))) 2345 (magit-insert-section (magit-module-section module t) 2346 (magit-insert-heading 2347 (propertize (concat "modified " module) 2348 'font-lock-face 'magit-diff-file-heading) 2349 " (" 2350 (cond (rewind "rewind") 2351 ((string-match-p "\\.\\.\\." range) "non-ff") 2352 (t "new commits")) 2353 (and (or modified untracked) 2354 (concat ", " 2355 (and modified "modified") 2356 (and modified untracked " and ") 2357 (and untracked "untracked") 2358 " content")) 2359 ")") 2360 (let ((default-directory 2361 (file-name-as-directory 2362 (expand-file-name module (magit-toplevel))))) 2363 (magit-git-wash (apply-partially 'magit-log-wash-log 'module) 2364 "log" "--oneline" "--left-right" range) 2365 (delete-char -1))))) 2366 ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ ]+\\) (\\([^)]+\\))$") 2367 (equal (match-string 1) module)) 2368 (magit-bind-match-strings (_module _range msg) nil 2369 (magit-delete-line) 2370 (magit-insert-section (magit-module-section module) 2371 (magit-insert-heading 2372 (propertize (concat "submodule " module) 2373 'font-lock-face 'magit-diff-file-heading) 2374 " (" msg ")")))) 2375 (t 2376 (magit-insert-section (magit-module-section module) 2377 (magit-insert-heading 2378 (propertize (concat "modified " module) 2379 'font-lock-face 'magit-diff-file-heading) 2380 " (" 2381 (and modified "modified") 2382 (and modified untracked " and ") 2383 (and untracked "untracked") 2384 " content)"))))))) 2385 2386 (defun magit-diff-wash-hunk () 2387 (when (looking-at "^@\\{2,\\} \\(.+?\\) @\\{2,\\}\\(?: \\(.*\\)\\)?") 2388 (let* ((heading (match-string 0)) 2389 (ranges (mapcar (lambda (str) 2390 (mapcar #'string-to-number 2391 (split-string (substring str 1) ","))) 2392 (split-string (match-string 1)))) 2393 (about (match-string 2)) 2394 (combined (= (length ranges) 3)) 2395 (value (cons about ranges))) 2396 (magit-delete-line) 2397 (magit-insert-section section (hunk value) 2398 (insert (propertize (concat heading "\n") 2399 'font-lock-face 'magit-diff-hunk-heading)) 2400 (magit-insert-heading) 2401 (while (not (or (eobp) (looking-at "^[^-+\s\\]"))) 2402 (forward-line)) 2403 (oset section end (point)) 2404 (oset section washer 'magit-diff-paint-hunk) 2405 (oset section combined combined) 2406 (if combined 2407 (oset section from-ranges (butlast ranges)) 2408 (oset section from-range (car ranges))) 2409 (oset section to-range (car (last ranges))) 2410 (oset section about about))) 2411 t)) 2412 2413 (defun magit-diff-expansion-threshold (section) 2414 "Keep new diff sections collapsed if washing takes too long." 2415 (and (magit-file-section-p section) 2416 (> (float-time (time-subtract (current-time) magit-refresh-start-time)) 2417 magit-diff-expansion-threshold) 2418 'hide)) 2419 2420 (add-hook 'magit-section-set-visibility-hook #'magit-diff-expansion-threshold) 2421 2422 ;;; Revision Mode 2423 2424 (define-derived-mode magit-revision-mode magit-diff-mode "Magit Rev" 2425 "Mode for looking at a Git commit. 2426 2427 This mode is documented in info node `(magit)Revision Buffer'. 2428 2429 \\<magit-mode-map>\ 2430 Type \\[magit-refresh] to refresh the current buffer. 2431 Type \\[magit-section-toggle] to expand or hide the section at point. 2432 Type \\[magit-visit-thing] to visit the hunk or file at point. 2433 2434 Staging and applying changes is documented in info node 2435 `(magit)Staging and Unstaging' and info node `(magit)Applying'. 2436 2437 \\<magit-hunk-section-map>Type \ 2438 \\[magit-apply] to apply the change at point, \ 2439 \\[magit-stage] to stage, 2440 \\[magit-unstage] to unstage, \ 2441 \\[magit-discard] to discard, or \ 2442 \\[magit-reverse] to reverse it. 2443 2444 \\{magit-revision-mode-map}" 2445 :group 'magit-revision 2446 (hack-dir-local-variables-non-file-buffer)) 2447 2448 (put 'magit-revision-mode 'magit-diff-default-arguments 2449 '("--stat" "--no-ext-diff")) 2450 2451 (defun magit-revision-setup-buffer (rev args files) 2452 (magit-setup-buffer #'magit-revision-mode nil 2453 (magit-buffer-revision rev) 2454 (magit-buffer-range (format "%s^..%s" rev rev)) 2455 (magit-buffer-diff-args args) 2456 (magit-buffer-diff-files files) 2457 (magit-buffer-diff-files-suspended nil))) 2458 2459 (defun magit-revision-refresh-buffer () 2460 (setq magit-buffer-revision-hash (magit-rev-parse magit-buffer-revision)) 2461 (magit-set-header-line-format 2462 (concat (magit-object-type magit-buffer-revision-hash) 2463 " " magit-buffer-revision 2464 (pcase (length magit-buffer-diff-files) 2465 (0) 2466 (1 (concat " limited to file " (car magit-buffer-diff-files))) 2467 (_ (concat " limited to files " 2468 (mapconcat #'identity magit-buffer-diff-files ", ")))))) 2469 (magit-insert-section (commitbuf) 2470 (magit-run-section-hook 'magit-revision-sections-hook))) 2471 2472 (cl-defmethod magit-buffer-value (&context (major-mode magit-revision-mode)) 2473 (cons magit-buffer-revision magit-buffer-diff-files)) 2474 2475 (defun magit-insert-revision-diff () 2476 "Insert the diff into this `magit-revision-mode' buffer." 2477 (magit--insert-diff 2478 "show" "-p" "--cc" "--format=" "--no-prefix" 2479 (and (member "--stat" magit-buffer-diff-args) "--numstat") 2480 magit-buffer-diff-args 2481 (concat magit-buffer-revision "^{commit}") 2482 "--" magit-buffer-diff-files)) 2483 2484 (defun magit-insert-revision-tag () 2485 "Insert tag message and headers into a revision buffer. 2486 This function only inserts anything when `magit-show-commit' is 2487 called with a tag as argument, when that is called with a commit 2488 or a ref which is not a branch, then it inserts nothing." 2489 (when (equal (magit-object-type magit-buffer-revision) "tag") 2490 (magit-insert-section (taginfo) 2491 (let ((beg (point))) 2492 ;; "git verify-tag -v" would output what we need, but the gpg 2493 ;; output is send to stderr and we have no control over the 2494 ;; order in which stdout and stderr are inserted, which would 2495 ;; make parsing hard. We are forced to use "git cat-file tag" 2496 ;; instead, which inserts the signature instead of verifying 2497 ;; it. We remove that later and then insert the verification 2498 ;; output using "git verify-tag" (without the "-v"). 2499 (magit-git-insert "cat-file" "tag" magit-buffer-revision) 2500 (goto-char beg) 2501 (forward-line 3) 2502 (delete-region beg (point))) 2503 (looking-at "^tagger \\([^<]+\\) <\\([^>]+\\)") 2504 (let ((heading (format "Tagger: %s <%s>" 2505 (match-string 1) 2506 (match-string 2)))) 2507 (magit-delete-line) 2508 (insert (propertize heading 'font-lock-face 2509 'magit-section-secondary-heading))) 2510 (magit-insert-heading) 2511 (if (re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t) 2512 (progn 2513 (let ((beg (match-beginning 0))) 2514 (re-search-forward "-----END PGP SIGNATURE-----") 2515 (delete-region beg (point))) 2516 (insert ?\n) 2517 (magit-process-git t "verify-tag" magit-buffer-revision)) 2518 (goto-char (point-max))) 2519 (insert ?\n)))) 2520 2521 (defvar magit-commit-message-section-map 2522 (let ((map (make-sparse-keymap))) 2523 (define-key map [remap magit-visit-thing] 'magit-show-commit) 2524 map) 2525 "Keymap for `commit-message' sections.") 2526 2527 (defun magit-insert-revision-message () 2528 "Insert the commit message into a revision buffer." 2529 (magit-insert-section section (commit-message) 2530 (oset section heading-highlight-face 'magit-diff-revision-summary-highlight) 2531 (let ((beg (point)) 2532 (rev magit-buffer-revision)) 2533 (insert (with-temp-buffer 2534 (magit-rev-insert-format "%B" rev) 2535 (magit-revision--wash-message))) 2536 (if (= (point) (+ beg 2)) 2537 (progn (backward-delete-char 2) 2538 (insert "(no message)\n")) 2539 (goto-char beg) 2540 (save-excursion 2541 (while (search-forward "\r\n" nil t) ; Remove trailing CRs. 2542 (delete-region (match-beginning 0) (1+ (match-beginning 0))))) 2543 (when magit-revision-fill-summary-line 2544 (let ((fill-column (min magit-revision-fill-summary-line 2545 (window-width)))) 2546 (fill-region (point) (line-end-position)))) 2547 (when magit-revision-use-hash-sections 2548 (save-excursion 2549 (while (not (eobp)) 2550 (re-search-forward "\\_<" nil 'move) 2551 (let ((beg (point))) 2552 (re-search-forward "\\_>" nil t) 2553 (when (> (point) beg) 2554 (let ((text (buffer-substring-no-properties beg (point)))) 2555 (when (pcase magit-revision-use-hash-sections 2556 (`quickest ; false negatives and positives 2557 (and (>= (length text) 7) 2558 (string-match-p "[0-9]" text) 2559 (string-match-p "[a-z]" text))) 2560 (`quicker ; false negatives (number-less hashes) 2561 (and (>= (length text) 7) 2562 (string-match-p "[0-9]" text) 2563 (magit-commit-p text))) 2564 (`quick ; false negatives (short hashes) 2565 (and (>= (length text) 7) 2566 (magit-commit-p text))) 2567 (`slow 2568 (magit-commit-p text))) 2569 (put-text-property beg (point) 2570 'font-lock-face 'magit-hash) 2571 (let ((end (point))) 2572 (goto-char beg) 2573 (magit-insert-section (commit text) 2574 (goto-char end)))))))))) 2575 (save-excursion 2576 (forward-line) 2577 (magit--add-face-text-property 2578 beg (point) 'magit-diff-revision-summary) 2579 (magit-insert-heading)) 2580 (when magit-diff-highlight-keywords 2581 (save-excursion 2582 (while (re-search-forward "\\[[^[]*\\]" nil t) 2583 (let ((beg (match-beginning 0)) 2584 (end (match-end 0))) 2585 (put-text-property 2586 beg end 'font-lock-face 2587 (if-let ((face (get-text-property beg 'font-lock-face))) 2588 (list face 'magit-keyword) 2589 'magit-keyword)))))) 2590 (goto-char (point-max)))))) 2591 2592 (defun magit-insert-revision-notes () 2593 "Insert commit notes into a revision buffer." 2594 (let* ((var "core.notesRef") 2595 (def (or (magit-get var) "refs/notes/commits"))) 2596 (dolist (ref (or (magit-list-active-notes-refs))) 2597 (magit-insert-section section (notes ref (not (equal ref def))) 2598 (oset section heading-highlight-face 'magit-diff-hunk-heading-highlight) 2599 (let ((beg (point)) 2600 (rev magit-buffer-revision)) 2601 (insert (with-temp-buffer 2602 (magit-git-insert "-c" (concat "core.notesRef=" ref) 2603 "notes" "show" rev) 2604 (magit-revision--wash-message))) 2605 (if (= (point) beg) 2606 (magit-cancel-section) 2607 (goto-char beg) 2608 (end-of-line) 2609 (insert (format " (%s)" 2610 (propertize (if (string-prefix-p "refs/notes/" ref) 2611 (substring ref 11) 2612 ref) 2613 'font-lock-face 'magit-refname))) 2614 (forward-char) 2615 (magit--add-face-text-property beg (point) 'magit-diff-hunk-heading) 2616 (magit-insert-heading) 2617 (goto-char (point-max)) 2618 (insert ?\n))))))) 2619 2620 (defun magit-revision--wash-message () 2621 (let ((major-mode 'git-commit-mode)) 2622 (hack-dir-local-variables) 2623 (hack-local-variables-apply)) 2624 (unless (memq git-commit-major-mode '(nil text-mode)) 2625 (funcall git-commit-major-mode) 2626 (font-lock-ensure)) 2627 (buffer-string)) 2628 2629 (defun magit-insert-revision-headers () 2630 "Insert headers about the commit into a revision buffer." 2631 (magit-insert-section (headers) 2632 (--when-let (magit-rev-format "%D" magit-buffer-revision "--decorate=full") 2633 (insert (magit-format-ref-labels it) ?\s)) 2634 (insert (propertize 2635 (magit-rev-parse (concat magit-buffer-revision "^{commit}")) 2636 'font-lock-face 'magit-hash)) 2637 (magit-insert-heading) 2638 (let ((beg (point))) 2639 (magit-rev-insert-format magit-revision-headers-format 2640 magit-buffer-revision) 2641 (magit-insert-revision-gravatars magit-buffer-revision beg)) 2642 (when magit-revision-insert-related-refs 2643 (dolist (parent (magit-commit-parents magit-buffer-revision)) 2644 (magit-insert-section (commit parent) 2645 (let ((line (magit-rev-format "%h %s" parent))) 2646 (string-match "^\\([^ ]+\\) \\(.*\\)" line) 2647 (magit-bind-match-strings (hash msg) line 2648 (insert "Parent: ") 2649 (insert (propertize hash 'font-lock-face 'magit-hash)) 2650 (insert " " msg "\n"))))) 2651 (magit--insert-related-refs 2652 magit-buffer-revision "--merged" "Merged" 2653 (eq magit-revision-insert-related-refs 'all)) 2654 (magit--insert-related-refs 2655 magit-buffer-revision "--contains" "Contained" 2656 (memq magit-revision-insert-related-refs '(all mixed))) 2657 (when-let ((follows (magit-get-current-tag magit-buffer-revision t))) 2658 (let ((tag (car follows)) 2659 (cnt (cadr follows))) 2660 (magit-insert-section (tag tag) 2661 (insert 2662 (format "Follows: %s (%s)\n" 2663 (propertize tag 'font-lock-face 'magit-tag) 2664 (propertize (number-to-string cnt) 2665 'font-lock-face 'magit-branch-local)))))) 2666 (when-let ((precedes (magit-get-next-tag magit-buffer-revision t))) 2667 (let ((tag (car precedes)) 2668 (cnt (cadr precedes))) 2669 (magit-insert-section (tag tag) 2670 (insert (format "Precedes: %s (%s)\n" 2671 (propertize tag 'font-lock-face 'magit-tag) 2672 (propertize (number-to-string cnt) 2673 'font-lock-face 'magit-tag)))))) 2674 (insert ?\n)))) 2675 2676 (defun magit--insert-related-refs (rev arg title remote) 2677 (when-let ((refs (magit-list-related-branches arg rev (and remote "-a")))) 2678 (insert title ":" (make-string (- 10 (length title)) ?\s)) 2679 (dolist (branch refs) 2680 (if (<= (+ (current-column) 1 (length branch)) 2681 (window-width)) 2682 (insert ?\s) 2683 (insert ?\n (make-string 12 ?\s))) 2684 (magit-insert-section (branch branch) 2685 (insert (propertize branch 'font-lock-face 2686 (if (string-prefix-p "remotes/" branch) 2687 'magit-branch-remote 2688 'magit-branch-local))))) 2689 (insert ?\n))) 2690 2691 (defun magit-insert-revision-gravatars (rev beg) 2692 (when (and magit-revision-show-gravatars 2693 (window-system)) 2694 (require 'gravatar) 2695 (pcase-let ((`(,author . ,committer) 2696 (pcase magit-revision-show-gravatars 2697 (`t '("^Author: " . "^Commit: ")) 2698 (`author '("^Author: " . nil)) 2699 (`committer '(nil . "^Commit: ")) 2700 (_ magit-revision-show-gravatars)))) 2701 (--when-let (and author (magit-rev-format "%aE" rev)) 2702 (magit-insert-revision-gravatar beg rev it author)) 2703 (--when-let (and committer (magit-rev-format "%cE" rev)) 2704 (magit-insert-revision-gravatar beg rev it committer))))) 2705 2706 (defun magit-insert-revision-gravatar (beg rev email regexp) 2707 (save-excursion 2708 (goto-char beg) 2709 (when (re-search-forward regexp nil t) 2710 (when-let ((window (get-buffer-window))) 2711 (let* ((column (length (match-string 0))) 2712 (font-obj (query-font (font-at (point) window))) 2713 (size (* 2 (aref font-obj 4))) 2714 (align-to (+ column 2715 (ceiling (/ size (aref font-obj 7) 1.0)) 2716 1)) 2717 (gravatar-size (- size 2))) 2718 (ignore-errors ; service may be unreachable 2719 (gravatar-retrieve email 'magit-insert-revision-gravatar-cb 2720 (list gravatar-size rev 2721 (point-marker) 2722 align-to column)))))))) 2723 2724 (defun magit-insert-revision-gravatar-cb (image size rev marker align-to column) 2725 (unless (eq image 'error) 2726 (when-let ((buffer (marker-buffer marker))) 2727 (with-current-buffer buffer 2728 (save-excursion 2729 (goto-char marker) 2730 ;; The buffer might display another revision by now or 2731 ;; it might have been refreshed, in which case another 2732 ;; process might already have inserted the image. 2733 (when (and (equal rev magit-buffer-revision) 2734 (not (eq (car-safe 2735 (car-safe 2736 (get-text-property (point) 'display))) 2737 'image))) 2738 (let ((top `((,@image 2739 :ascent center :relief 1 :scale 1 :height ,size) 2740 (slice 0.0 0.0 1.0 0.5))) 2741 (bot `((,@image 2742 :ascent center :relief 1 :scale 1 :height ,size) 2743 (slice 0.0 0.5 1.0 1.0))) 2744 (align `((space :align-to ,align-to)))) 2745 (when magit-revision-use-gravatar-kludge 2746 (cl-rotatef top bot)) 2747 (let ((inhibit-read-only t)) 2748 (insert (propertize " " 'display top)) 2749 (insert (propertize " " 'display align)) 2750 (forward-line) 2751 (forward-char column) 2752 (insert (propertize " " 'display bot)) 2753 (insert (propertize " " 'display align)))))))))) 2754 2755 ;;; Merge-Preview Mode 2756 2757 (define-derived-mode magit-merge-preview-mode magit-diff-mode "Magit Merge" 2758 "Mode for previewing a merge." 2759 :group 'magit-diff 2760 (hack-dir-local-variables-non-file-buffer)) 2761 2762 (put 'magit-merge-preview-mode 'magit-diff-default-arguments 2763 '("--no-ext-diff")) 2764 2765 (defun magit-merge-preview-setup-buffer (rev) 2766 (magit-setup-buffer #'magit-merge-preview-mode nil 2767 (magit-buffer-revision rev) 2768 (magit-buffer-range (format "%s^..%s" rev rev)))) 2769 2770 (defun magit-merge-preview-refresh-buffer () 2771 (let* ((branch (magit-get-current-branch)) 2772 (head (or branch (magit-rev-verify "HEAD")))) 2773 (magit-set-header-line-format (format "Preview merge of %s into %s" 2774 magit-buffer-revision 2775 (or branch "HEAD"))) 2776 (magit-insert-section (diffbuf) 2777 (magit--insert-diff 2778 "merge-tree" (magit-git-string "merge-base" head magit-buffer-revision) 2779 head magit-buffer-revision)))) 2780 2781 (cl-defmethod magit-buffer-value (&context (major-mode magit-merge-preview-mode)) 2782 magit-buffer-revision) 2783 2784 ;;; Diff Sections 2785 2786 (defun magit-hunk-set-window-start (section) 2787 "When SECTION is a `hunk', ensure that its beginning is visible. 2788 It the SECTION has a different type, then do nothing." 2789 (when (magit-hunk-section-p section) 2790 (magit-section-set-window-start section))) 2791 2792 (add-hook 'magit-section-movement-hook #'magit-hunk-set-window-start) 2793 2794 (defun magit-hunk-goto-successor (section arg) 2795 (and (magit-hunk-section-p section) 2796 (when-let ((parent (magit-get-section 2797 (magit-section-ident 2798 (oref section parent))))) 2799 (let* ((children (oref parent children)) 2800 (siblings (magit-section-siblings section 'prev)) 2801 (previous (nth (length siblings) children))) 2802 (if (not arg) 2803 (--when-let (or previous (car (last children))) 2804 (magit-section-goto it) 2805 t) 2806 (when previous 2807 (magit-section-goto previous)) 2808 (if (and (stringp arg) 2809 (re-search-forward arg (oref parent end) t)) 2810 (goto-char (match-beginning 0)) 2811 (goto-char (oref (car (last children)) end)) 2812 (forward-line -1) 2813 (while (looking-at "^ ") (forward-line -1)) 2814 (while (looking-at "^[-+]") (forward-line -1)) 2815 (forward-line))))))) 2816 2817 (add-hook 'magit-section-goto-successor-hook #'magit-hunk-goto-successor) 2818 2819 (defvar magit-unstaged-section-map 2820 (let ((map (make-sparse-keymap))) 2821 (define-key map [remap magit-visit-thing] 'magit-diff-unstaged) 2822 (define-key map [remap magit-delete-thing] 'magit-discard) 2823 (define-key map "s" 'magit-stage) 2824 (define-key map "u" 'magit-unstage) 2825 map) 2826 "Keymap for the `unstaged' section.") 2827 2828 (magit-define-section-jumper magit-jump-to-unstaged "Unstaged changes" unstaged) 2829 2830 (defun magit-insert-unstaged-changes () 2831 "Insert section showing unstaged changes." 2832 (magit-insert-section (unstaged) 2833 (magit-insert-heading "Unstaged changes:") 2834 (magit--insert-diff 2835 "diff" magit-buffer-diff-args "--no-prefix" 2836 "--" magit-buffer-diff-files))) 2837 2838 (defvar magit-staged-section-map 2839 (let ((map (make-sparse-keymap))) 2840 (define-key map [remap magit-visit-thing] 'magit-diff-staged) 2841 (define-key map [remap magit-delete-thing] 'magit-discard) 2842 (define-key map [remap magit-revert-no-commit] 'magit-reverse) 2843 (define-key map "s" 'magit-stage) 2844 (define-key map "u" 'magit-unstage) 2845 map) 2846 "Keymap for the `staged' section.") 2847 2848 (magit-define-section-jumper magit-jump-to-staged "Staged changes" staged) 2849 2850 (defun magit-insert-staged-changes () 2851 "Insert section showing staged changes." 2852 ;; Avoid listing all files as deleted when visiting a bare repo. 2853 (unless (magit-bare-repo-p) 2854 (magit-insert-section (staged) 2855 (magit-insert-heading "Staged changes:") 2856 (magit--insert-diff 2857 "diff" "--cached" magit-buffer-diff-args "--no-prefix" 2858 "--" magit-buffer-diff-files)))) 2859 2860 ;;; Diff Type 2861 2862 (defun magit-diff-type (&optional section) 2863 "Return the diff type of SECTION. 2864 2865 The returned type is one of the symbols `staged', `unstaged', 2866 `committed', or `undefined'. This type serves a similar purpose 2867 as the general type common to all sections (which is stored in 2868 the `type' slot of the corresponding `magit-section' struct) but 2869 takes additional information into account. When the SECTION 2870 isn't related to diffs and the buffer containing it also isn't 2871 a diff-only buffer, then return nil. 2872 2873 Currently the type can also be one of `tracked' and `untracked' 2874 but these values are not handled explicitly everywhere they 2875 should be and a possible fix could be to just return nil here. 2876 2877 The section has to be a `diff' or `hunk' section, or a section 2878 whose children are of type `diff'. If optional SECTION is nil, 2879 return the diff type for the current section. In buffers whose 2880 major mode is `magit-diff-mode' SECTION is ignored and the type 2881 is determined using other means. In `magit-revision-mode' 2882 buffers the type is always `committed'. 2883 2884 Do not confuse this with `magit-diff-scope' (which see)." 2885 (--when-let (or section (magit-current-section)) 2886 (cond ((derived-mode-p 'magit-revision-mode 'magit-stash-mode) 'committed) 2887 ((derived-mode-p 'magit-diff-mode) 2888 (let ((range magit-buffer-range) 2889 (const magit-buffer-typearg)) 2890 (cond ((equal const "--no-index") 'undefined) 2891 ((or (not range) 2892 (magit-rev-eq range "HEAD")) 2893 (if (equal const "--cached") 2894 'staged 2895 'unstaged)) 2896 ((equal const "--cached") 2897 (if (magit-rev-head-p range) 2898 'staged 2899 'undefined)) ; i.e. committed and staged 2900 (t 'committed)))) 2901 ((derived-mode-p 'magit-status-mode) 2902 (let ((stype (oref it type))) 2903 (if (memq stype '(staged unstaged tracked untracked)) 2904 stype 2905 (pcase stype 2906 ((or `file `module) 2907 (let* ((parent (oref it parent)) 2908 (type (oref parent type))) 2909 (if (memq type '(file module)) 2910 (magit-diff-type parent) 2911 type))) 2912 (`hunk (thread-first it 2913 (oref parent) 2914 (oref parent) 2915 (oref type))))))) 2916 ((derived-mode-p 'magit-log-mode) 2917 (if (or (and (magit-section-match 'commit section) 2918 (oref section children)) 2919 (magit-section-match [* file commit] section)) 2920 'committed 2921 'undefined)) 2922 (t 'undefined)))) 2923 2924 (cl-defun magit-diff-scope (&optional (section nil ssection) strict) 2925 "Return the diff scope of SECTION or the selected section(s). 2926 2927 A diff's \"scope\" describes what part of a diff is selected, it is 2928 a symbol, one of `region', `hunk', `hunks', `file', `files', or 2929 `list'. Do not confuse this with the diff \"type\", as returned by 2930 `magit-diff-type'. 2931 2932 If optional SECTION is non-nil, then return the scope of that, 2933 ignoring the sections selected by the region. Otherwise return 2934 the scope of the current section, or if the region is active and 2935 selects a valid group of diff related sections, the type of these 2936 sections, i.e. `hunks' or `files'. If SECTION, or if that is nil 2937 the current section, is a `hunk' section; and the region region 2938 starts and ends inside the body of a that section, then the type 2939 is `region'. If the region is empty after a mouse click, then 2940 `hunk' is returned instead of `region'. 2941 2942 If optional STRICT is non-nil, then return nil if the diff type of 2943 the section at point is `untracked' or the section at point is not 2944 actually a `diff' but a `diffstat' section." 2945 (let ((siblings (and (not ssection) (magit-region-sections nil t)))) 2946 (setq section (or section (car siblings) (magit-current-section))) 2947 (when (and section 2948 (or (not strict) 2949 (and (not (eq (magit-diff-type section) 'untracked)) 2950 (not (eq (--when-let (oref section parent) 2951 (oref it type)) 2952 'diffstat))))) 2953 (pcase (list (oref section type) 2954 (and siblings t) 2955 (magit-diff-use-hunk-region-p) 2956 ssection) 2957 (`(hunk nil t ,_) 2958 (if (magit-section-internal-region-p section) 'region 'hunk)) 2959 (`(hunk t t nil) 'hunks) 2960 (`(hunk ,_ ,_ ,_) 'hunk) 2961 (`(file t t nil) 'files) 2962 (`(file ,_ ,_ ,_) 'file) 2963 (`(module t t nil) 'files) 2964 (`(module ,_ ,_ ,_) 'file) 2965 (`(,(or `staged `unstaged `untracked) nil ,_ ,_) 'list))))) 2966 2967 (defun magit-diff-use-hunk-region-p () 2968 (and (region-active-p) 2969 ;; TODO implement this from first principals 2970 ;; currently it's trial-and-error 2971 (not (and (or (eq this-command 'mouse-drag-region) 2972 (eq last-command 'mouse-drag-region) 2973 ;; When another window was previously 2974 ;; selected then the last-command is 2975 ;; some byte-code function. 2976 (byte-code-function-p last-command)) 2977 (eq (region-end) (region-beginning)))))) 2978 2979 ;;; Diff Highlight 2980 2981 (add-hook 'magit-section-unhighlight-hook #'magit-diff-unhighlight) 2982 (add-hook 'magit-section-highlight-hook #'magit-diff-highlight) 2983 2984 (defun magit-diff-unhighlight (section selection) 2985 "Remove the highlighting of the diff-related SECTION." 2986 (when (magit-hunk-section-p section) 2987 (magit-diff-paint-hunk section selection nil) 2988 t)) 2989 2990 (defun magit-diff-highlight (section selection) 2991 "Highlight the diff-related SECTION. 2992 If SECTION is not a diff-related section, then do nothing and 2993 return nil. If SELECTION is non-nil, then it is a list of sections 2994 selected by the region, including SECTION. All of these sections 2995 are highlighted." 2996 (if (and (magit-section-match 'commit section) 2997 (oref section children)) 2998 (progn (if selection 2999 (dolist (section selection) 3000 (magit-diff-highlight-list section selection)) 3001 (magit-diff-highlight-list section)) 3002 t) 3003 (when-let ((scope (magit-diff-scope section t))) 3004 (cond ((eq scope 'region) 3005 (magit-diff-paint-hunk section selection t)) 3006 (selection 3007 (dolist (section selection) 3008 (magit-diff-highlight-recursive section selection))) 3009 (t 3010 (magit-diff-highlight-recursive section))) 3011 t))) 3012 3013 (defun magit-diff-highlight-recursive (section &optional selection) 3014 (pcase (magit-diff-scope section) 3015 (`list (magit-diff-highlight-list section selection)) 3016 (`file (magit-diff-highlight-file section selection)) 3017 (`hunk (magit-diff-highlight-heading section selection) 3018 (magit-diff-paint-hunk section selection t)) 3019 (_ (magit-section-highlight section nil)))) 3020 3021 (defun magit-diff-highlight-list (section &optional selection) 3022 (let ((beg (oref section start)) 3023 (cnt (oref section content)) 3024 (end (oref section end))) 3025 (when (or (eq this-command 'mouse-drag-region) 3026 (not selection)) 3027 (unless (and (region-active-p) 3028 (<= (region-beginning) beg)) 3029 (magit-section-make-overlay beg cnt 'magit-section-highlight)) 3030 (unless (oref section hidden) 3031 (dolist (child (oref section children)) 3032 (when (or (eq this-command 'mouse-drag-region) 3033 (not (and (region-active-p) 3034 (<= (region-beginning) 3035 (oref child start))))) 3036 (magit-diff-highlight-recursive child selection))))) 3037 (when magit-diff-highlight-hunk-body 3038 (magit-section-make-overlay (1- end) end 'magit-section-highlight)))) 3039 3040 (defun magit-diff-highlight-file (section &optional selection) 3041 (magit-diff-highlight-heading section selection) 3042 (unless (oref section hidden) 3043 (dolist (child (oref section children)) 3044 (magit-diff-highlight-recursive child selection)))) 3045 3046 (defun magit-diff-highlight-heading (section &optional selection) 3047 (magit-section-make-overlay 3048 (oref section start) 3049 (or (oref section content) 3050 (oref section end)) 3051 (pcase (list (oref section type) 3052 (and (member section selection) 3053 (not (eq this-command 'mouse-drag-region)))) 3054 (`(file t) 'magit-diff-file-heading-selection) 3055 (`(file nil) 'magit-diff-file-heading-highlight) 3056 (`(module t) 'magit-diff-file-heading-selection) 3057 (`(module nil) 'magit-diff-file-heading-highlight) 3058 (`(hunk t) 'magit-diff-hunk-heading-selection) 3059 (`(hunk nil) 'magit-diff-hunk-heading-highlight)))) 3060 3061 ;;; Hunk Paint 3062 3063 (cl-defun magit-diff-paint-hunk 3064 (section &optional selection 3065 (highlight (magit-section-selected-p section selection))) 3066 (let (paint) 3067 (unless magit-diff-highlight-hunk-body 3068 (setq highlight nil)) 3069 (cond (highlight 3070 (unless (oref section hidden) 3071 (add-to-list 'magit-section-highlighted-sections section) 3072 (cond ((memq section magit-section-unhighlight-sections) 3073 (setq magit-section-unhighlight-sections 3074 (delq section magit-section-unhighlight-sections))) 3075 (magit-diff-highlight-hunk-body 3076 (setq paint t))))) 3077 (t 3078 (cond ((and (oref section hidden) 3079 (memq section magit-section-unhighlight-sections)) 3080 (add-to-list 'magit-section-highlighted-sections section) 3081 (setq magit-section-unhighlight-sections 3082 (delq section magit-section-unhighlight-sections))) 3083 (t 3084 (setq paint t))))) 3085 (when paint 3086 (save-excursion 3087 (goto-char (oref section start)) 3088 (let ((end (oref section end)) 3089 (merging (looking-at "@@@")) 3090 (diff-type (magit-diff-type)) 3091 (stage nil) 3092 (tab-width (magit-diff-tab-width 3093 (magit-section-parent-value section)))) 3094 (forward-line) 3095 (while (< (point) end) 3096 (when (and magit-diff-hide-trailing-cr-characters 3097 (char-equal ?\r (char-before (line-end-position)))) 3098 (put-text-property (1- (line-end-position)) (line-end-position) 3099 'invisible t)) 3100 (put-text-property 3101 (point) (1+ (line-end-position)) 'font-lock-face 3102 (cond 3103 ((looking-at "^\\+\\+?\\([<=|>]\\)\\{7\\}") 3104 (setq stage (pcase (list (match-string 1) highlight) 3105 (`("<" nil) 'magit-diff-our) 3106 (`("<" t) 'magit-diff-our-highlight) 3107 (`("|" nil) 'magit-diff-base) 3108 (`("|" t) 'magit-diff-base-highlight) 3109 (`("=" nil) 'magit-diff-their) 3110 (`("=" t) 'magit-diff-their-highlight) 3111 (`(">" nil) nil))) 3112 'magit-diff-conflict-heading) 3113 ((looking-at (if merging "^\\(\\+\\| \\+\\)" "^\\+")) 3114 (magit-diff-paint-tab merging tab-width) 3115 (magit-diff-paint-whitespace merging 'added diff-type) 3116 (or stage 3117 (if highlight 'magit-diff-added-highlight 'magit-diff-added))) 3118 ((looking-at (if merging "^\\(-\\| -\\)" "^-")) 3119 (magit-diff-paint-tab merging tab-width) 3120 (magit-diff-paint-whitespace merging 'removed diff-type) 3121 (if highlight 'magit-diff-removed-highlight 'magit-diff-removed)) 3122 (t 3123 (magit-diff-paint-tab merging tab-width) 3124 (magit-diff-paint-whitespace merging 'context diff-type) 3125 (if highlight 'magit-diff-context-highlight 'magit-diff-context)))) 3126 (forward-line)))))) 3127 (magit-diff-update-hunk-refinement section)) 3128 3129 (defvar magit-diff--tab-width-cache nil) 3130 3131 (defun magit-diff-tab-width (file) 3132 (setq file (expand-file-name file)) 3133 (cl-flet ((cache (value) 3134 (let ((elt (assoc file magit-diff--tab-width-cache))) 3135 (if elt 3136 (setcdr elt value) 3137 (setq magit-diff--tab-width-cache 3138 (cons (cons file value) 3139 magit-diff--tab-width-cache)))) 3140 value)) 3141 (cond 3142 ((not magit-diff-adjust-tab-width) 3143 tab-width) 3144 ((--when-let (find-buffer-visiting file) 3145 (cache (buffer-local-value 'tab-width it)))) 3146 ((--when-let (assoc file magit-diff--tab-width-cache) 3147 (or (cdr it) 3148 tab-width))) 3149 ((or (eq magit-diff-adjust-tab-width 'always) 3150 (and (numberp magit-diff-adjust-tab-width) 3151 (>= magit-diff-adjust-tab-width 3152 (nth 7 (file-attributes file))))) 3153 (cache (buffer-local-value 'tab-width (find-file-noselect file)))) 3154 (t 3155 (cache nil) 3156 tab-width)))) 3157 3158 (defun magit-diff-paint-tab (merging width) 3159 (save-excursion 3160 (forward-char (if merging 2 1)) 3161 (while (= (char-after) ?\t) 3162 (put-text-property (point) (1+ (point)) 3163 'display (list (list 'space :width width))) 3164 (forward-char)))) 3165 3166 (defun magit-diff-paint-whitespace (merging line-type diff-type) 3167 (when (and magit-diff-paint-whitespace 3168 (or (not (memq magit-diff-paint-whitespace '(uncommitted status))) 3169 (memq diff-type '(staged unstaged))) 3170 (cl-case line-type 3171 (added t) 3172 (removed (memq magit-diff-paint-whitespace-lines '(all both))) 3173 (context (memq magit-diff-paint-whitespace-lines '(all))))) 3174 (let ((prefix (if merging "^[-\\+\s]\\{2\\}" "^[-\\+\s]")) 3175 (indent 3176 (if (local-variable-p 'magit-diff-highlight-indentation) 3177 magit-diff-highlight-indentation 3178 (setq-local 3179 magit-diff-highlight-indentation 3180 (cdr (--first (string-match-p (car it) default-directory) 3181 (nreverse 3182 (default-value 3183 'magit-diff-highlight-indentation)))))))) 3184 (when (and magit-diff-highlight-trailing 3185 (looking-at (concat prefix ".*?\\([ \t]+\\)$"))) 3186 (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t))) 3187 (overlay-put ov 'font-lock-face 'magit-diff-whitespace-warning) 3188 (overlay-put ov 'priority 2) 3189 (overlay-put ov 'evaporate t))) 3190 (when (or (and (eq indent 'tabs) 3191 (looking-at (concat prefix "\\( *\t[ \t]*\\)"))) 3192 (and (integerp indent) 3193 (looking-at (format "%s\\([ \t]* \\{%s,\\}[ \t]*\\)" 3194 prefix indent)))) 3195 (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t))) 3196 (overlay-put ov 'font-lock-face 'magit-diff-whitespace-warning) 3197 (overlay-put ov 'priority 2) 3198 (overlay-put ov 'evaporate t)))))) 3199 3200 (defun magit-diff-update-hunk-refinement (&optional section) 3201 (if section 3202 (unless (oref section hidden) 3203 (pcase (list magit-diff-refine-hunk 3204 (oref section refined) 3205 (eq section (magit-current-section))) 3206 ((or `(all nil ,_) `(t nil t)) 3207 (oset section refined t) 3208 (save-excursion 3209 (goto-char (oref section start)) 3210 ;; `diff-refine-hunk' does not handle combined diffs. 3211 (unless (looking-at "@@@") 3212 (let ((smerge-refine-ignore-whitespace 3213 magit-diff-refine-ignore-whitespace) 3214 ;; Avoid fsyncing many small temp files 3215 (write-region-inhibit-fsync t)) 3216 (diff-refine-hunk))))) 3217 ((or `(nil t ,_) `(t t nil)) 3218 (oset section refined nil) 3219 (remove-overlays (oref section start) 3220 (oref section end) 3221 'diff-mode 'fine)))) 3222 (cl-labels ((recurse (section) 3223 (if (magit-section-match 'hunk section) 3224 (magit-diff-update-hunk-refinement section) 3225 (dolist (child (oref section children)) 3226 (recurse child))))) 3227 (recurse magit-root-section)))) 3228 3229 3230 ;;; Hunk Region 3231 3232 (defun magit-diff-hunk-region-beginning () 3233 (save-excursion (goto-char (region-beginning)) 3234 (line-beginning-position))) 3235 3236 (defun magit-diff-hunk-region-end () 3237 (save-excursion (goto-char (region-end)) 3238 (line-end-position))) 3239 3240 (defun magit-diff-update-hunk-region (section) 3241 "Highlight the hunk-internal region if any." 3242 (when (and (eq (oref section type) 'hunk) 3243 (eq (magit-diff-scope section t) 'region)) 3244 (magit-diff--make-hunk-overlay 3245 (oref section start) 3246 (1- (oref section content)) 3247 'font-lock-face 'magit-diff-lines-heading 3248 'display (magit-diff-hunk-region-header section) 3249 'after-string (magit-diff--hunk-after-string 'magit-diff-lines-heading)) 3250 (run-hook-with-args 'magit-diff-highlight-hunk-region-functions section) 3251 t)) 3252 3253 (defun magit-diff-highlight-hunk-region-dim-outside (section) 3254 "Dim the parts of the hunk that are outside the hunk-internal region. 3255 This is done by using the same foreground and background color 3256 for added and removed lines as for context lines." 3257 (let ((face (if magit-diff-highlight-hunk-body 3258 'magit-diff-context-highlight 3259 'magit-diff-context))) 3260 (when magit-diff-unmarked-lines-keep-foreground 3261 (setq face `(,@(and (>= emacs-major-version 27) '(:extend t)) 3262 :background ,(face-attribute face :background)))) 3263 (magit-diff--make-hunk-overlay (oref section content) 3264 (magit-diff-hunk-region-beginning) 3265 'font-lock-face face 3266 'priority 2) 3267 (magit-diff--make-hunk-overlay (1+ (magit-diff-hunk-region-end)) 3268 (oref section end) 3269 'font-lock-face face 3270 'priority 2))) 3271 3272 (defun magit-diff-highlight-hunk-region-using-face (_section) 3273 "Highlight the hunk-internal region by making it bold. 3274 Or rather highlight using the face `magit-diff-hunk-region', though 3275 changing only the `:weight' and/or `:slant' is recommended for that 3276 face." 3277 (magit-diff--make-hunk-overlay (magit-diff-hunk-region-beginning) 3278 (1+ (magit-diff-hunk-region-end)) 3279 'font-lock-face 'magit-diff-hunk-region)) 3280 3281 (defun magit-diff-highlight-hunk-region-using-overlays (section) 3282 "Emphasize the hunk-internal region using delimiting horizontal lines. 3283 This is implemented as single-pixel newlines places inside overlays." 3284 (if (window-system) 3285 (let ((beg (magit-diff-hunk-region-beginning)) 3286 (end (magit-diff-hunk-region-end)) 3287 (str (propertize 3288 (concat (propertize "\s" 'display '(space :height (1))) 3289 (propertize "\n" 'line-height t)) 3290 'font-lock-face 'magit-diff-lines-boundary))) 3291 (magit-diff--make-hunk-overlay beg (1+ beg) 'before-string str) 3292 (magit-diff--make-hunk-overlay end (1+ end) 'after-string str)) 3293 (magit-diff-highlight-hunk-region-using-face section))) 3294 3295 (defun magit-diff-highlight-hunk-region-using-underline (section) 3296 "Emphasize the hunk-internal region using delimiting horizontal lines. 3297 This is implemented by overlining and underlining the first and 3298 last (visual) lines of the region." 3299 (if (window-system) 3300 (let* ((beg (magit-diff-hunk-region-beginning)) 3301 (end (magit-diff-hunk-region-end)) 3302 (beg-eol (save-excursion (goto-char beg) 3303 (end-of-visual-line) 3304 (point))) 3305 (end-bol (save-excursion (goto-char end) 3306 (beginning-of-visual-line) 3307 (point))) 3308 (color (face-background 'magit-diff-lines-boundary nil t))) 3309 (cl-flet ((ln (b e &rest face) 3310 (magit-diff--make-hunk-overlay 3311 b e 'font-lock-face face 'after-string 3312 (magit-diff--hunk-after-string face)))) 3313 (if (= beg end-bol) 3314 (ln beg beg-eol :overline color :underline color) 3315 (ln beg beg-eol :overline color) 3316 (ln end-bol end :underline color)))) 3317 (magit-diff-highlight-hunk-region-using-face section))) 3318 3319 (defun magit-diff--make-hunk-overlay (start end &rest args) 3320 (let ((ov (make-overlay start end nil t))) 3321 (overlay-put ov 'evaporate t) 3322 (while args (overlay-put ov (pop args) (pop args))) 3323 (push ov magit-section--region-overlays) 3324 ov)) 3325 3326 (defun magit-diff--hunk-after-string (face) 3327 (propertize "\s" 3328 'font-lock-face face 3329 'display (list 'space :align-to 3330 `(+ (0 . right) 3331 ,(min (window-hscroll) 3332 (- (line-end-position) 3333 (line-beginning-position))))) 3334 ;; This prevents the cursor from being rendered at the 3335 ;; edge of the window. 3336 'cursor t)) 3337 3338 ;;; Hunk Utilities 3339 3340 (defun magit-diff-inside-hunk-body-p () 3341 "Return non-nil if point is inside the body of a hunk." 3342 (and (magit-section-match 'hunk) 3343 (when-let ((content (oref (magit-current-section) content))) 3344 (> (point) content)))) 3345 3346 ;;; Diff Extract 3347 3348 (defun magit-diff-file-header (section &optional no-rename) 3349 (when (magit-hunk-section-p section) 3350 (setq section (oref section parent))) 3351 (and (magit-file-section-p section) 3352 (let ((header (oref section header))) 3353 (if no-rename 3354 (replace-regexp-in-string 3355 "^--- \\(.+\\)" (oref section value) header t t 1) 3356 header)))) 3357 3358 (defun magit-diff-hunk-region-header (section) 3359 (let ((patch (magit-diff-hunk-region-patch section))) 3360 (string-match "\n" patch) 3361 (substring patch 0 (1- (match-end 0))))) 3362 3363 (defun magit-diff-hunk-region-patch (section &optional args) 3364 (let ((op (if (member "--reverse" args) "+" "-")) 3365 (sbeg (oref section start)) 3366 (rbeg (magit-diff-hunk-region-beginning)) 3367 (rend (region-end)) 3368 (send (oref section end)) 3369 (patch nil)) 3370 (save-excursion 3371 (goto-char sbeg) 3372 (while (< (point) send) 3373 (looking-at "\\(.\\)\\([^\n]*\n\\)") 3374 (cond ((or (string-match-p "[@ ]" (match-string-no-properties 1)) 3375 (and (>= (point) rbeg) 3376 (<= (point) rend))) 3377 (push (match-string-no-properties 0) patch)) 3378 ((equal op (match-string-no-properties 1)) 3379 (push (concat " " (match-string-no-properties 2)) patch))) 3380 (forward-line))) 3381 (let ((buffer-list-update-hook nil)) ; #3759 3382 (with-temp-buffer 3383 (insert (mapconcat #'identity (reverse patch) "")) 3384 (diff-fixup-modifs (point-min) (point-max)) 3385 (setq patch (buffer-string)))) 3386 patch)) 3387 3388 ;;; _ 3389 (provide 'magit-diff) 3390 ;;; magit-diff.el ends here