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