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