;;; km-magit.el --- Magit extensions ;; Copyright (C) 2012-2016 Kyle Meyer ;; Author: Kyle Meyer ;; URL: https://github.com/kyleam/emacs.d ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Code: (require 'avy) (require 'git-rebase) (require 'km-util) (require 'magit) (require 'projectile) (defun km/magit-status (&optional default-display) "Like `magit-status', but change the meaning of the prefix argument. If DEFAULT-DISPLAY is non-nil, use `magit-display-buffer-traditional' as the value for `magit-display-buffer-function'." (interactive "P") (let ((magit-display-buffer-function (if default-display #'magit-display-buffer-traditional magit-display-buffer-function)) (current-prefix-arg nil)) (call-interactively #'magit-status))) (defun km/magit-auto-commit () "Commit all changes with \"auto\" commit message. This can be useful for non-source code repos (e.g., Org mode note files) or commiting incomplete changes that will be extended into a proper commit." (interactive) (magit-run-git "commit" "--all" "--message=auto")) (defun km/magit-update-or-auto-commit () (interactive) (let ((files (delete-dups (nconc (magit-modified-files) (magit-staged-files))))) (cl-case (length files) (0 (user-error "No tracked files with changes")) (1 (magit-run-git "commit" "--all" "--message" (concat (car files) ": Update"))) (t (km/magit-auto-commit))))) ;;;###autoload (defun km/magit-show-commit-at-point (&optional choose-project) "Show commit point. If there is no current project or if the prefix argument CHOOSE-PROJECT is non-nil, prompt for the project name." (interactive "P") (if (save-excursion (skip-chars-backward "A-z0-9") (looking-at "\\b[A-z0-9]\\{4,40\\}\\b")) (let* ((hash (match-string-no-properties 0)) (project (and (or choose-project (not (projectile-project-p)) (not (magit-rev-verify (concat hash "^{commit}")))) (completing-read "Project: " (projectile-relevant-known-projects)))) (default-directory (or project default-directory))) (magit-show-commit hash (car (magit-diff-arguments)))) (user-error "No hash found at point"))) ;;;###autoload (defun km/magit-commit-extend-with-file () "Extend last commit with changes in the current file." (interactive) (let ((file (or (magit-current-file) (user-error "No current file")))) (cond ((magit-anything-staged-p) (user-error "There are already staged changes")) ((member file (nconc (magit-untracked-files) (magit-modified-files))) (magit-with-toplevel (magit-stage-file file)) (magit-commit-extend)) (t (message "No changes to %s" file))))) ;;;###autoload (defun km/magit-commit-wip-with-file () "Make a WIP commit for the current file. Unlike `magit-wip-*' commands, this commit is made to the current branch." (interactive) (let ((file (or (magit-current-file) (user-error "No current file")))) (cond ((magit-anything-staged-p) (user-error "There are already staged changes")) ((member file (nconc (magit-untracked-files) (magit-modified-files))) (magit-with-toplevel (magit-stage-file file)) (magit-run-git "commit" (concat "--message=WIP " file))) (t (message "No changes to %s" file))))) (defun km/magit-ff-merge-upstream () "Perform fast-forward merge of upstream branch. \n(git merge --no-edit --ff-only )" (interactive) (--if-let (magit-get-upstream-branch) (magit-merge it '("--ff-only")) (user-error "No upstream branch"))) (defun km/magit-push-all () "Push all branches." (interactive) (magit-run-git-async "push" "-v" (magit-read-remote "Remote") "--all")) (defun km/magit-push-head (remote &optional args) "Push current branch to same name on remote. \n(git push [ARGS] REMOTE HEAD)" (interactive (list (magit-read-remote "Remote") (magit-push-arguments))) (magit-run-git-async "push" "-v" args remote "HEAD")) (defun km/magit-checkout-local-tracking (remote-branch) "Create and checkout a local tracking branch for REMOTE-BRANCH. \n(git checkout -t REMOTE-BRANCH\)" (interactive (list (let ((branches (magit-list-remote-branch-names))) (magit-completing-read "Remote branch" branches nil t nil nil (car (member (magit-branch-or-commit-at-point) branches)))))) (magit-run-git "checkout" "-t" remote-branch)) (defun km/magit-branch-rename (old new &optional force) "Like `magit-branch-rename', but use old branch as initial prompt." (interactive (let ((branch (magit-read-local-branch "Rename branch"))) (list branch (magit-read-string-ns (format "Rename branch '%s' to" branch) branch) current-prefix-arg))) (unless (string= old new) (magit-run-git "branch" (if force "-M" "-m") old new))) (defun km/magit-delete-previous-branch (&optional force) "Delete previous branch. \n(git branch -d @{-1})" (interactive "P") (magit-run-git "branch" (if force "-D" "-d") "@{-1}")) (defun km/magit-checkout-previous-branch () "Checkout previous branch. \n(git checkout -)" (interactive) (magit-run-git "checkout" "-")) (defun km/magit-list-recent-refs (n &optional remote) "List N recent refs. If REMOTE is non-nil, limit to remote refs." (magit-git-lines "for-each-ref" "--sort=-committerdate" "--format=%(refname:short)" (format "--count=%s" n) (if remote "refs/remotes" "refs/heads"))) (defun km/magit-checkout-recent-ref (n) "Checkout branch from N recent refs. Refs are sorted by committer date." (interactive (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg)) 5))) (magit-run-git "checkout" (magit-completing-read "Ref" (km/magit-list-recent-refs n)))) (defun km/magit-checkout-track-recent-ref (n) "Create and checkout a local tracking branch. Listed refs are limited to N most recent, sorted by committer date." (interactive (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg)) 5))) (magit-run-git "checkout" "-t" (magit-completing-read "Ref" (km/magit-list-recent-refs n 'remote)))) (defun km/magit-checkout-master () "Checkout master branch. \n(git checkout master)" (interactive) (magit-run-git "checkout" "master")) (defun km/magit-branch-and-checkout-from-current (branch) "Create and checkout BRANCH at current branch. This is equivalent to running `magit-branch-and-checkout' with START-POINT set to the current branch. \n(git checkout -b BRANCH)" (interactive (list (magit-read-string "Branch name" nil nil (--when-let (magit-get-current-branch) (concat it "-tmp"))))) (magit-run-git "checkout" "-b" branch)) (defun km/magit-branch-backup-current (&optional choose-name) "Create a backup branch for the current branch. With the prefix argument CHOOSE-NAME, prompt for the name of the backup branch. Otherwise, name it as 'b/__v', where is incremented to form a refname that doesn't already exist." (interactive "P") (let ((current (or (magit-get-current-branch) (user-error "No current branch")))) (magit-run-git "branch" (if choose-name (magit-read-string-ns "Backup name" current) (let* ((version-re (format "\\`%s__v\\([[:digit:]]+\\)\\'" current)) (versions (delq nil (mapcar (lambda (s) (and (string-match version-re s) (string-to-number (match-string-no-properties 1 s)))) (magit-list-local-branch-names))))) (format "%s__v%d" current (if (null versions) 1 (1+ (apply #'max versions))))))))) (defun km/magit-mode-bury-all-windows (&optional kill-buffer) "Run `magit-mode-quit-window' until no longer in Magit buffer." (interactive "P") (while (derived-mode-p 'magit-mode) (magit-mode-bury-buffer kill-buffer))) (defun km/magit-log-select-guess-fixup-commit (&optional ntop) "Guess commit from fixup/squash commmits. Consider NTOP commits (default is 5) when searching for 'fixup!' and 'squash!' titles." (interactive (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg)) 5))) (let (ntop-end msgs commit-pts) (save-excursion ;; Get limit for fixup/squash search. (goto-char (point-min)) (setq ntop-end (line-end-position (1+ ntop))) ;; Get fixup and squash messages. (while (re-search-forward "[a-z0-9]+ \\(fixup!\\|squash!\\) \\(.+\\)" ntop-end t) (push (match-string-no-properties 2) msgs)) (when (not msgs) (user-error "No fixup or squash commits found")) ;; Find earliest commit. (dolist (msg msgs) (goto-char (point-min)) (when (re-search-forward (concat "[a-z0-9]+ " msg "\n") nil t) (push (match-beginning 0) commit-pts)))) (if commit-pts (goto-char (apply #'max commit-pts)) (message "No matching commits found")))) ;;;###autoload (defun km/magit-reset-file (rev file &optional checkout) "Reset FILE from revision REV. If prefix argument CHECKOUT is non-nil, checkout FILE from REV instead. \(git reset REV -- FILE) \(git checkout REV -- FILE)" (interactive (let ((rev (magit-read-branch-or-commit "Revision" magit-buffer-revision))) (list rev (magit-read-file-from-rev rev "File") current-prefix-arg))) (magit-with-toplevel (magit-run-git (if checkout "checkout" "reset") rev "--" file))) ;;;###autoload (defun km/magit-pin-file (&optional other-rev) "Pin this file to the current revision. Visit the current file and current revision with `magit-find-file'. Position point as in the original buffer. This may not correspond to same content if text before point has changed since the current commit. If OTHER-REV is non-nil, prompt for another revision instead of the current. If buffer is already a revision buffer, then find the working tree copy instead. In both these cases, point may not land in a reasonable location depending on how the content of the file has changed." (interactive "P") (magit-with-toplevel (let* ((line (+ (if (bolp) 1 0) (count-lines 1 (point)))) (col (current-column)) (rev (cond (other-rev (magit-read-branch-or-commit "Find file from revision")) ((not magit-buffer-file-name) (or (magit-get-current-branch) (magit-rev-parse "HEAD"))))) (fname (file-relative-name (or buffer-file-name magit-buffer-file-name (user-error "Buffer not visiting file"))))) (if rev (magit-find-file rev fname) (find-file fname)) (goto-char (point-min)) (forward-line (1- line)) (move-to-column col)))) ;;;###autoload (defun km/magit-revfile-reset (&optional checkout) "Reset to revision from current revfile. If CHECKOUT is non-nil, checkout file instead." (interactive "P") (unless (and magit-buffer-refname magit-buffer-file-name) (user-error "Not in Magit revfile buffer")) (magit-with-toplevel (magit-run-git (if checkout "checkout" "reset") magit-buffer-refname "--" magit-buffer-file-name))) ;;;###autoload (defun km/magit-find-recently-changed-file (&optional n) "Find a file that changed from \"HEAD~N..HEAD\". N defaults to 20." (interactive (list (and current-prefix-arg (prefix-numeric-value current-prefix-arg)))) (setq n (or n 10)) (magit-with-toplevel (find-file (magit-completing-read "File" (magit-changed-files (format "HEAD~%s..HEAD" n)) nil t)))) (defun km/magit-find-commit-file (commit) "Find file changed in COMMIT." (interactive (list (or (magit-branch-or-commit-at-point) (and (derived-mode-p 'magit-revision-mode) (car magit-refresh-args)) (magit-read-branch-or-commit "Commit")))) (let ((files (magit-changed-files (format "%s~..%s" commit commit)))) (find-file (cl-case (length files) (0 (user-error "No changed files in %s" commit)) (1 (car files)) (t (magit-completing-read "File" files nil t)))))) (defun km/magit-insert-staged-file (&optional no-directory) "Select staged file to insert. This is useful for referring to file names in commit messages. By default, the path for the file name is relative to the top directory of the repository. Remove the directory component from the file name if NO-DIRECTORY is non-nil. If there are no staged files, look instead at files that changed in HEAD. These rules will usually offer the files of interest while commiting, but this is not the case if you are amending a commit with the \"--only\" flag and have staged files (i.e., this command will still offer the staged files)." (interactive "P") (magit-with-toplevel (let* ((files (or (magit-staged-files) (magit-changed-files "HEAD^..HEAD"))) (file (cl-case (length files) (1 (car files)) (0 (error "No files found")) (t (completing-read "Staged file: " files nil t))))) (insert (if no-directory (file-name-nondirectory file) file))))) (defun km/magit-shorten-hash (hash &optional n) (magit-rev-parse (format "--short=%s" (or n (magit-abbrev-length))) hash)) ;;;###autoload (defun km/magit-shorten-hash-at-point (&optional n) "Shorten hash at point to N characters. N defaults to `magit-abbrev-length'. If the commit belongs to the current repo and the hash is ambiguous, the hash is extended as needed. To explicitly set the hash length, use a numeric prefix argument." (interactive (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg)) (magit-abbrev-length)))) (cond ((< n 4) (user-error "Hash must be at least 4 characters")) ((>= n 40) (user-error "Full hashes are 40 characters")) ((> n 30) (message "That doesn't seem incredibly useful, but OK"))) (let ((offset (- (skip-chars-backward "A-z0-9")))) (if (looking-at "\\b[A-z0-9]\\{5,40\\}\\b") (let ((hash-len (- (match-end 0) (match-beginning 0))) (hash (match-string 0))) (when (< hash-len n) (user-error "Desired hash length is greater than current")) (replace-match (or (km/magit-shorten-hash hash n) ;; We're not in a repo. (substring hash 0 n)) 'fixedcase) (when (< offset n) (skip-chars-backward "A-z0-9") (goto-char (+ (point) offset)))) (goto-char (+ (point) offset)) (user-error "No hash found at point")))) (defvar km/magit-copy-functions '(km/magit-copy-commit-summary-from-header km/magit-copy-commit-message km/magit-copy-region-commits km/magit-copy-region-hunk km/magit-copy-hunk) "Functions tried by `km/magit-copy-as-kill'. These will be given one argument (the current prefix value) and should succeed by copying and returning non-nil or fail by returning nil.") ;;;###autoload (defun km/magit-copy-commit-summary (commit) "Copy a citation for the COMMIT at point. Format the reference as ', (, )'. If there is no commit at point or with a prefix argument, prompt for COMMIT." (interactive (let ((atpoint (or (and magit-blame-mode (magit-blame-chunk-get :hash)) (magit-branch-or-commit-at-point) (magit-tag-at-point)))) (list (or (and (not current-prefix-arg) atpoint) (magit-read-branch-or-commit "Commit" atpoint))))) (if (magit-rev-verify (concat commit "^{commit}")) (kill-new (message "%s" ;; Using `magit-git-string' instead of ;; `magit-rev-format' to pass --date flag. (magit-git-string "show" "-s" "--date=short" "--format=%h (%s, %ad)" commit "--"))) (user-error "%s does not exist" commit))) (defun km/magit-copy-commit-summary-from-header (&optional _) (magit-section-when headers (km/magit-copy-commit-summary (car magit-refresh-args)))) (defun km/magit-copy-region-commits (&optional read-separator) (--when-let (magit-region-values 'commit) (deactivate-mark) (kill-new (message "%s" (mapconcat #'identity it (if read-separator (read-string "Separator: ") ", ")))))) (defun km/magit-copy--truncated-message (msg) (let ((msg-lines (split-string msg "[\n\r]+" 'omit-nulls))) (message "%s" (cl-case (length msg-lines) (0 msg) (1 (car msg-lines)) (t (concat (car msg-lines) "[...]")))))) (defun km/magit-copy-commit-message (&optional _) (magit-section-when message (let ((msg (buffer-substring-no-properties (magit-section-start it) (magit-section-end it)))) (kill-new msg) (km/magit-copy--truncated-message msg)))) (defun km/magit-copy-region-hunk (&optional no-column) (when (magit-section-internal-region-p) (magit-section-when hunk (deactivate-mark) (let ((text (buffer-substring-no-properties (region-beginning) (region-end)))) (setq text (if no-column (replace-regexp-in-string "^[ \\+\\-]" "" text) text)) (kill-new text) (km/magit-copy--truncated-message text))))) (defun km/magit-copy-hunk (&optional _) (magit-section-when hunk (kill-new (buffer-substring-no-properties (save-excursion (goto-char (magit-section-start it)) (1+ (point-at-eol))) (magit-section-end it))) (message "Copied hunk: %s" (magit-section-value it)))) (defun km/magit-copy-as-kill () "Try `km/magit-copy-functions' before calling `magit-copy-section-value'. With a prefix argument of -1, always call `magit-copy-section-value' Otherwise, the current prefix argument is passed to each hook function." (interactive) (or (unless (= (prefix-numeric-value current-prefix-arg) -1) (run-hook-with-args-until-success 'km/magit-copy-functions current-prefix-arg)) (magit-copy-section-value))) (defun km/magit-describe (rev) "Run 'git describe' on REV." (interactive (list (or (-when-let (section (magit-current-section)) (cond ((memq (magit-section-type section) '(commit branch)) (magit-section-value section)) ((derived-mode-p 'magit-revision-mode) (car magit-refresh-args)))) (magit-read-branch-or-commit "Revision")))) (--when-let (and rev (magit-git-string "describe" rev)) (kill-new (message it)))) (defun km/magit-rev-ancestor-p (rev-a rev-b) "Report whether REV-A is the ancestor of REV-B. Use the revision at point as REV-B. With prefix argument or if there is no revision at point, prompt for the revision. Always prompt for REV-A." (interactive (let* ((atpoint (or (and magit-blame-mode (magit-blame-chunk-get :hash)) (magit-branch-or-commit-at-point) (magit-tag-at-point))) (commit (or (and (not current-prefix-arg) atpoint) (magit-read-branch-or-commit "Descendant" atpoint)))) (list (magit-read-other-branch-or-commit (format "Test if ancestor of %s" commit) commit) commit))) (message "%s is %san ancestor of %s" rev-a (if (magit-git-success "merge-base" "--is-ancestor" rev-a rev-b) "" "NOT ") rev-b)) (defun km/magit-refs-toggle-tags () "Toggle showing tags in `magit-refs-mode'. This only affects the current buffer and is useful if you do not show tags by default." (interactive) (if (memq 'magit-insert-tags magit-refs-sections-hook) (remove-hook 'magit-refs-sections-hook 'magit-insert-tags t) (add-hook 'magit-refs-sections-hook 'magit-insert-tags t t)) (magit-refresh-buffer)) (defun km/magit-revision-insert-related-refs () (interactive) (let ((magit-revision-insert-related-refs t)) (magit-refresh))) (defun km/magit-log-flip-revs () "Swap revisions in log range." (interactive) (let ((range (caar magit-refresh-args))) (if (and range (derived-mode-p 'magit-log-mode) (string-match magit-range-re range)) (progn (setf (caar magit-refresh-args) (concat (match-string 3 range) (match-string 2 range) (match-string 1 range))) (magit-refresh)) (user-error "No range to swap")))) (defun km/magit-cherry-flip-revs () (interactive) (pcase-let ((`(,rev0 ,rev1) magit-refresh-args)) (setf (nth 0 magit-refresh-args) rev1) (setf (nth 1 magit-refresh-args) rev0) (magit-refresh))) (defun km/magit-flip-revs () (interactive) (cond ((derived-mode-p 'magit-diff-mode) (call-interactively #'magit-diff-flip-revs)) ((derived-mode-p 'magit-log-mode) (call-interactively #'km/magit-log-flip-revs)) ((derived-mode-p 'magit-cherry-mode) (call-interactively #'km/magit-cherry-flip-revs)))) (defun km/magit-log-modify-range () "Change range for current log buffer." (interactive) (unless (derived-mode-p 'magit-log-mode) (user-error "Not in log buffer")) (setf (caar magit-refresh-args) (read-string "Range: " (caar magit-refresh-args))) (magit-refresh)) (defun km/magit-cherry-insert-in-upstream () (insert ?\n) (magit-insert-section (cherries) (magit-insert-heading "In upstream:") (magit-git-wash (apply-partially 'magit-log-wash-log 'cherry) "cherry" "-v" "--abbrev" (nth 1 magit-refresh-args) (nth 0 magit-refresh-args)))) (defun km/magit-cherry-toggle-upstream-section () (interactive) (let ((pos (point))) (if (memq #'km/magit-cherry-insert-in-upstream magit-cherry-sections-hook) (kill-local-variable 'magit-cherry-sections-hook) (setq-local magit-cherry-sections-hook (append magit-cherry-sections-hook '(km/magit-cherry-insert-in-upstream)))) (magit-refresh-buffer) (goto-char pos))) (defun km/magit-diff-visit-file (&optional prev-rev other-window) "Like `magit-diff-visit-file', but with the option to visit REV^. If prefix argument PREV-REV is non-nil, visit file for REV^ instead of REV. If not in `magit-revision-mode', the prefix argument has no effect. OTHER-WINDOW corresponds to `magit-diff-visit-file's OTHER-WINDOW argument. Interactively, this can be accessed using the command `km/magit-diff-visit-file-other-window'." (interactive "P") (let ((magit-refresh-args (if (and prev-rev (derived-mode-p 'magit-revision-mode)) (cons (concat (car magit-refresh-args) "^") (cdr magit-refresh-args)) magit-refresh-args)) (current-prefix-arg (and other-window (list 4)))) (call-interactively #'magit-diff-visit-file))) (defun km/magit-diff-visit-file-other-window (&optional prev-rev) (interactive "P") (km/magit-diff-visit-file prev-rev t)) (defun km/magit-stash-edit-message (stash message) "Change STASH's message to MESSAGE." (interactive (let* ((stash (magit-read-stash "Rename" t)) (old-msg (magit-git-string "show" "-s" "--format=%s" stash))) (list stash (magit-read-string "Stash message" old-msg)))) (let ((commit (magit-rev-parse stash)) (inhibit-magit-refresh t)) (magit-stash-drop stash) (magit-stash-store message "refs/stash" commit)) (magit-refresh)) (defun km/git-commit-turn-on-flyspell () "Like `git-commit-turn-on-flyspell', but don't check buffer initially." (setq flyspell-generic-check-word-predicate 'git-commit-flyspell-verify) (turn-on-flyspell)) (defun km/magit-log-dwim (&optional args files) (interactive (magit-log-arguments)) (let ((range (magit-section-case ((unpushed unpulled) (magit-section-value it)) (tag (concat (magit-section-value it) "..")) (branch (let ((current (magit-get-current-branch)) (atpoint (magit-section-value it)) (upstream (magit-get-upstream-branch)) (push (magit-get-push-branch))) (cond ((equal atpoint current) (and upstream (concat upstream ".."))) ((equal atpoint push) (concat push "..")) (t (concat ".." atpoint)))))))) (if range (magit-log (list range) args files) (call-interactively #'magit-log)))) (defun km/magit-cherry-dwim () (interactive) (-let [(head . upstream) (if (eq major-mode 'magit-log-mode) (let ((range (caar magit-refresh-args))) (and range (string-match magit-range-re range) (cons (match-string 3 range) (match-string 1 range)))) (let ((section (magit-current-section)) (current-branch (magit-get-current-branch))) (pcase (list (magit-section-type section) (magit-section-value section)) (`(unpushed "@{upstream}..") (cons current-branch (magit-get-upstream-branch))) (`(unpulled "..@{upstream}") (cons (magit-get-upstream-branch) current-branch)) ;; Don't try to match "@{push}" because ;; `magit-insert-unpulled-from-pushremote' and ;; `magit-insert-unpulled-from-pushremote' avoid it to ;; be compatible with all push.default settings. (`(unpushed ,_) (cons current-branch (magit-get-push-branch))) (`(unpulled ,_) (cons (magit-get-push-branch) current-branch)))))] (if (and head upstream) (magit-cherry head upstream) (call-interactively #'magit-cherry)))) (defun km/magit--insert-count-lines (rev counts) (-let [(n-behind n-ahead) counts] (when (> n-ahead 0) (magit-insert-section (unpushed (concat rev "..")) (magit-insert-heading (format "%3s ahead of %s" (propertize (number-to-string n-ahead) 'face 'magit-diffstat-added) rev)))) (when (> n-behind 0) (magit-insert-section (unpulled (concat ".." rev)) (magit-insert-heading (format "%3s behind %s" (propertize (number-to-string n-behind) 'face 'magit-diffstat-removed) rev)))))) (defun km/magit-insert-remote-counts () "Insert section showing number of unpushed and unpulled commits. This function is a lightweight replacement of four `magit-status-sections-hook' functions: `magit-insert-unpulled-from-upstream', `magit-insert-unpulled-from-pushremote', `magit-insert-unpushed-to-upstream', and `magit-insert-unpushed-to-pushremote'. Unlike the above functions, this function does not insert a log of unpulled or unpushed commits, but instead inserts a combined section that only reports the number of commits in each category." (when (magit-get-current-branch) (let* ((up-counts (and (magit-git-success "rev-parse" "@{upstream}") (magit-rev-diff-count "@{upstream}" ""))) ;; Unlike `magit-insert-unpushed-to-pushremote' and ;; `magit-insert-unpulled-from-pushremote', just use ;; "@{push}". This drops support for the push-remote if ;; push.default isn't set to a compatible value. See ;; Magit's 6505f4cd. (pu-counts (and (magit-git-success "rev-parse" "@{push}") (magit-rev-diff-count "@{push}" ""))) (up-any (and up-counts (or (> (car up-counts) 0) (> (cadr up-counts) 0)))) (pu-any (and pu-counts (or (> (car pu-counts) 0) (> (cadr pu-counts) 0))))) (when (or up-any pu-any) (magit-insert-section (remote-counts) (magit-insert-heading "Remote counts") (when up-any (km/magit--insert-count-lines "@{upstream}" up-counts)) (when (and pu-any (not (and up-any (equal (magit-rev-name "@{upstream}") (magit-rev-name "@{push}"))))) (km/magit--insert-count-lines "@{push}" pu-counts))))))) (magit-define-section-jumper magit-jump-to-remote-counts "Remote counts" remote-counts) ;;; Git Rebase mode (defun km/git-rebase--clean-subject (s) (replace-regexp-in-string (concat "\\`" (regexp-opt '("fixup! " "squash! "))) "" s)) (defun km/git-rebase-fixup-duplicates (beg end &optional squash) "Mark sequential lines with same subject as fixup commits. With an active region, limit to lines that the region touches. If prefix argument SQUASH is non-nil, mark for squashing instead of fixing up." (interactive (nconc (km/region-or-buffer-line-bounds) (list current-prefix-arg))) (unless (markerp end) (setq end (copy-marker end))) (save-excursion (goto-char beg) (let ((prefix (if squash "squash" "fixup")) prev-subj subj) (while (re-search-forward git-rebase-line end t) (setq subj (km/git-rebase--clean-subject (match-string-no-properties 3))) (when (equal subj prev-subj) (let ((inhibit-read-only t)) (replace-match prefix 'fixedcase nil nil 1))) (setq prev-subj subj))))) (defun km/git-rebase-join-repeats (beg end &optional arg) "Move repeated subject lines after line of first occurrence. If region is active, limit to lines that the region touches. By default, repeated lines are marked for fixing up. With a \\[universal-argument], mark them for squashing instead. With a \\[universal-argument] \\[universal-argument], do not mark them at all." (interactive (nconc (km/region-or-buffer-line-bounds) (list current-prefix-arg))) (save-excursion (goto-char beg) (let (roots dups) (while (re-search-forward git-rebase-line end t) (let ((subj (km/git-rebase--clean-subject (match-string-no-properties 3)))) (push (list subj (match-string-no-properties 0) (point-marker)) (if (assoc subj roots) dups roots)))) (pcase-dolist (`(,subj ,line ,marker) dups) (goto-char (1+ (nth 2 (assoc subj roots)))) (let ((inhibit-read-only t)) (insert (concat line "\n")) (goto-char marker) (delete-region (point-at-bol) (1+ (point-at-eol))))) (unless (equal arg (list 16)) (km/git-rebase-fixup-duplicates beg end (equal arg (list 4))))))) (defun km/git-rebase--move-line (new-pos) (let ((ln-beg (point-at-bol)) (ln-end (1+ (point-at-eol)))) (goto-char new-pos) (let ((inhibit-read-only t)) (insert (delete-and-extract-region ln-beg ln-end))) (forward-line -1))) (defun km/git-rebase--collect-lines () (let ((current-ln (point-at-bol)) pt candidates) (save-excursion (save-restriction (narrow-to-region (window-start) (window-end)) (goto-char (point-min)) (while (re-search-forward git-rebase-line nil t) (setq pt (point-at-bol)) (unless (= pt current-ln) (push pt candidates))) ;; Offer first empty line after last commit as candidate so ;; the current commit can be moved to the end. (unless (> current-ln (car candidates)) (forward-line 1) (push (point-at-bol) candidates)) (nreverse candidates))))) (defun km/git-rebase-move-commit () "Move commit on current line above selected line." (interactive) (unless (save-excursion (beginning-of-line) (looking-at-p git-rebase-line)) (user-error "Not on commit line")) (avy-with km/git-rebase-move-commit (setq avy-action #'km/git-rebase--move-line) (avy--process (km/git-rebase--collect-lines) #'avy--overlay-post))) (provide 'km-magit) ;;; km-magit.el ends here