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