;;; emacs-conflict.el --- quickly find and resolve conflicts in external tools like Syncthing -*- lexical-binding:t -*- ;; Copyright (C) 2019 Pierre Penninckx ;; Author: Pierre Penninckx ;; URL: https://github.com/ibizaman/emacs-conflicts ;; Version: 0.1.0 ;; This file 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 file 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 file. If not, see . ;;; Commentary: ;; `emacs-conflict' is used to quickly find and resolve conflicts in ;; external tools like Syncthing, Nextcloud or Pacman. ;; ;; `emacs-conflict-resolve-conflicts' searches a given directory for ;; all conflict files and provides a list of all of them. The ;; inconvenience is this function works synchronously so it will block ;; Emacs. ;; ;; `emacs-conflict-show-conflicts-dired' is the asynchronous version ;; where the results are presented in a `dired' buffer thanks to ;; `find-name-dired'. In the `dired' buffer, hover a conflict file ;; then call `emacs-conflict-resolve-conflict-dired'. ;; ;; In both cases, the conflict will be resolved using an `ediff' ;; session. ;; ;; The tool supports `Syncthing' [1], 'Nextcloud' [2] and 'Pacman' [3] ;; for now. ;; ;; [1] https://docs.syncthing.net/users/faq.html#what-if-there-is-a-conflict ;; [2] https://docs.nextcloud.com/desktop/2.6/conflicts.html ;; [3] https://wiki.archlinux.org/title/Pacman/Pacnew_and_Pacsave ;;; Code: (require 'dired) (require 'dired-aux) (require 'ediff) (defgroup conflict nil "Find conflicting files" :group 'files :prefix "emacs-conflict") (defcustom emacs-conflict-find-regexes '(("syncthing" "\\.sync-conflict-.*\\(\\.\\)" "\\1") ("nextcloud" " (conflicted copy .*)\\(\\.\\)" "\\1") ("pacman" "\\(?:\\.\\)pacnew$" "")) "Regexes to identify a file as a conflict." :type '(alist :key-type string :value-type (list regexp regexp)) :group 'conflict) (defun emacs-conflict--find-regexes () "Merge all regexes into one." (mapconcat (lambda (ls) (nth 1 ls)) emacs-conflict-find-regexes "\\|")) (defun emacs-conflict-resolve-conflicts (directory) "Resolve all conflicts under given DIRECTORY." (interactive "D") (let* ((all (emacs-conflict--get-sync-conflicts directory)) (chosen (emacs-conflict--pick-a-conflict all))) (emacs-conflict--resolve-conflict chosen))) (defun emacs-conflict-show-conflicts-dired (directory) "Open dired buffer at DIRECTORY showing all syncthing conflicts." (interactive "D") (find-lisp-find-dired directory (emacs-conflict--find-regexes))) (defun emacs-conflict-resolve-conflict-dired (&optional arg) "Resolve conflict of first marked file in dired or close to point with ARG." (interactive "P") (let* ((chosen (car (dired-get-marked-files nil arg)))) (emacs-conflict--resolve-conflict chosen))) (defun emacs-conflict--resolve-conflict (conflict) "Resolve CONFLICT file using ediff." (let* ((normal (emacs-conflict--get-normal-filename conflict))) (emacs-conflict--resolve-ediff (list conflict normal) `(lambda () (when (y-or-n-p "Delete conflict file? ") (kill-buffer (get-file-buffer ,conflict)) (delete-file ,conflict)))))) (defun emacs-conflict--get-sync-conflicts (directory) "Return a list of all sync conflict files in a DIRECTORY." (directory-files-recursively directory (emacs-conflict--find-regexes))) (defvar emacs-conflict--conflict-history nil "Completion conflict history.") (defun emacs-conflict--pick-a-conflict (conflicts) "Let user choose the next conflict from CONFLICTS to investigate." (completing-read "Choose the conflict to investigate: " conflicts nil t nil 'emacs-conflict--conflict-history)) (defun emacs-conflict--get-normal-filename (conflict) "Get non-conflict filename matching the given CONFLICT." (let (normal-filename) (dolist (r emacs-conflict-find-regexes normal-filename) (let ((regex (nth 1 r)) (replacement (nth 2 r))) (when (and (null normal-filename) (not (null (string-match-p regex conflict)))) (setq normal-filename (replace-regexp-in-string regex replacement conflict))))))) (defun emacs-conflict--resolve-ediff (&optional files quit-hook) "Resolve conflict between files using `ediff'. If FILES is nil, conflict resolution will be done between the two marked files in `dired'. QUIT-HOOK, if given is called ." (let ((files (or files (dired-get-marked-files))) (quit-hook quit-hook) (wnd (current-window-configuration))) (if (<= (length files) 2) (let ((file1 (car files)) (file2 (if (cdr files) (cadr files) (read-file-name "file: " (dired-dwim-target-directory))))) (if (file-newer-than-file-p file1 file2) (ediff-files file2 file1) (ediff-files file1 file2)) (add-hook 'ediff-after-quit-hook-internal (lambda () (setq ediff-after-quit-hook-internal nil) (when quit-hook (funcall quit-hook)) (set-window-configuration wnd)))) (error "No more than 2 files should be marked")))) (provide 'emacs-conflict) ;;; emacs-conflict.el ends here