;;; ssh-deploy-diff-mode.el --- Mode for interactive directory differences -*- lexical-binding:t -*-
;; Copyright (C) 2017-2023 Free Software Foundation, Inc.
;; This file is not part of GNU Emacs.
;; 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, 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 GNU Emacs. If not, see .
;;; Commentary:
;; Please see README.md from the same repository for extended documentation.
;;; Code:
(require 'ssh-deploy)
(defconst ssh-deploy-diff-mode--keywords
'(
"DIRECTORY A"
"DIRECTORY B"
"EXCLUDE-LIST"
"FILES ONLY IN A"
"FILES ONLY IN B"
"FILES IN BOTH BUT DIFFERS"
"HELP"
)
"Use list of keywords to build regular expression for syntax highlighting.")
(defconst ssh-deploy-diff-mode--font-lock-keywords
(let ((regex (concat "\\<" (regexp-opt ssh-deploy-diff-mode--keywords t) "\\>")))
(list
`(,regex . font-lock-builtin-face)
'("\\('\\w*'\\)" . font-lock-variable-name-face)))
"Minimal highlighting expressions for SSH Deploy Diff major mode.")
(defvar ssh-deploy-diff-mode-map
(let ((map (make-keymap)))
(define-key map "C" 'ssh-deploy-diff-mode-copy-handler)
(define-key map "a" 'ssh-deploy-diff-mode-copy-a-handler)
(define-key map "b" 'ssh-deploy-diff-mode-copy-b-handler)
(define-key map "D" 'ssh-deploy-diff-mode-delete-handler)
(define-key map (kbd "") 'ssh-deploy-diff-mode-difference-handler)
(define-key map "g" 'ssh-deploy-diff-mode-refresh-handler)
(define-key map (kbd "") 'ssh-deploy-diff-mode-open-handler)
(define-key map (kbd "") 'ssh-deploy-diff-mode-open-handler)
map)
"Key-map for SSH Deploy Diff major mode.")
(defun ssh-deploy-diff-mode-copy-handler() "Start the copy action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--copy))
(defun ssh-deploy-diff-mode-copy-a-handler() "Start the copy A action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--copy-a))
(defun ssh-deploy-diff-mode-copy-b-handler() "Start the copy B action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--copy-b))
(defun ssh-deploy-diff-mode-delete-handler() "Start the delete action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--delete))
(defun ssh-deploy-diff-mode-difference-handler() "Start the difference action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--difference))
(defun ssh-deploy-diff-mode-refresh-handler() "Start the refresh action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--refresh))
(defun ssh-deploy-diff-mode-open-handler() "Start the open action." (interactive)(ssh-deploy-diff-mode--action-handler #'ssh-deploy-diff-mode--open))
(defun ssh-deploy-diff-mode--get-parts ()
"Return current file and section if any."
(interactive)
(save-excursion
(beginning-of-line)
(let ((file nil))
(when (looking-at "^- ")
(let* ((start (+ 2 (line-beginning-position)))
(end (line-end-position)))
(setq file (buffer-substring-no-properties start end))))
(while (and (> (line-number-at-pos) 1)
(not (looking-at "^[A-Z]+")))
(forward-line -1))
(when (looking-at "^[A-Z]")
(let* ((start (line-beginning-position))
(end (line-end-position))
(section (buffer-substring-no-properties start end)))
(setq section (replace-regexp-in-string ": ([0-9]+)\\'" "" section))
(setq section
(pcase section
("DIRECTORY A" 'directory-a)
("DIRECTORY B" 'directory-b)
("EXCLUDE-LIST" 'exclude-list)
("FILES ONLY IN A" 'only-in-a)
("FILES ONLY IN B" 'only-in-b)
("FILES IN BOTH BUT DIFFERS" 'in-both)
(_ (message "Could not find section %s" section)
section)))
(while (and (> (line-number-at-pos) 1)
(not (looking-at "^DIRECTORY B:")))
(forward-line -1))
(when (looking-at "^DIRECTORY B:")
(let* ((start (line-beginning-position))
(end (line-end-position))
(directory-b (buffer-substring-no-properties start end)))
(setq directory-b (replace-regexp-in-string "DIRECTORY B: " "" directory-b))
(while (and (> (line-number-at-pos) 1)
(not (looking-at "^DIRECTORY A:")))
(forward-line -1))
(when (looking-at "^DIRECTORY A:")
(let* ((start (line-beginning-position))
(end (line-end-position))
(directory-a (buffer-substring-no-properties start end)))
(setq directory-a (replace-regexp-in-string "DIRECTORY A: " "" directory-a))
(list file section directory-a directory-b))))))))))
(defun ssh-deploy-diff-mode--action-handler (action)
"Route valid ACTION to their functions."
(interactive)
(let ((parts (ssh-deploy-diff-mode--get-parts)))
(unless (eq parts nil)
(cond
((null parts) (message "Found nothing to do"))
((not (or (nth 0 parts)
;; FIXME: Comparing equality of functions is bad karma!
(eq action #'ssh-deploy-diff-mode--refresh)))
(message "Found nothing to do in the section for action %s"
(replace-regexp-in-string "ssh-deploy-diff-mode--" ""
(format "%s" action))))
(t (funcall action parts))))))
(defun ssh-deploy-diff-mode--refresh (parts)
"Refresh current difference query based on PARTS."
(interactive)
(let ((root-local (nth 2 parts))
(root-remote (nth 3 parts)))
(kill-this-buffer)
(ssh-deploy-diff-directories root-local root-remote)))
(defun ssh-deploy-diff-mode--copy (parts)
"Perform an upload or download depending on section in PARTS."
(let* ((file-name (nth 0 parts))
(root-local (file-truename (nth 2 parts)))
(root-remote (nth 3 parts))
(path-local (file-truename (expand-file-name file-name root-local)))
(path-remote (expand-file-name file-name root-remote))
(section (nth 1 parts)))
(pcase section
('only-in-a
(ssh-deploy-upload path-local path-remote 1))
('only-in-b
(ssh-deploy-download path-remote path-local))
(_ (message "Copy is not available in this section")))))
(defun ssh-deploy-diff-mode--copy-a (parts)
"Perform a upload of local-path to remote-path based on PARTS
from section A or section BOTH."
(let* ((section (nth 1 parts))
(file-name (nth 0 parts))
(root-local (file-truename (nth 2 parts)))
(root-remote (nth 3 parts))
(path-local (file-truename (expand-file-name file-name root-local)))
(path-remote (expand-file-name file-name root-remote)))
(cond ((memq section '(only-in-a in-both))
(ssh-deploy-upload path-local path-remote 1))
(t (message "Copy A is not available in this section")))))
(defun ssh-deploy-diff-mode--copy-b (parts)
"Perform an download of remote-path to local-path based on PARTS
from section B or section BOTH."
(let* ((section (nth 1 parts))
(file-name (nth 0 parts))
(root-local (file-truename (nth 2 parts)))
(root-remote (nth 3 parts))
(path-local (file-truename (expand-file-name file-name root-local)))
(path-remote (expand-file-name file-name root-remote)))
(cond ((memq section '(only-in-b in-both))
(ssh-deploy-download path-remote path-local))
(t (message "Copy B is not available in this section")))))
(defun ssh-deploy-diff-mode--delete (parts)
"Delete path in both, only in a or only in b based on PARTS
from section A, B or BOTH."
(let* ((section (nth 1 parts))
(file-name (nth 0 parts))
(root-local (nth 2 parts))
(root-remote (nth 3 parts))
(path-local (file-truename (expand-file-name file-name root-local)))
(path-remote (expand-file-name file-name root-remote)))
(pcase section
('in-both
(let ((yes-no-prompt (read-string (format "Type 'yes' to confirm that you want to delete the file '%s': " file-name))))
(when (string= yes-no-prompt "yes")
(ssh-deploy-delete-both path-local))))
('only-in-a
(ssh-deploy-delete path-local))
('only-in-b
(ssh-deploy-delete path-remote))
(_ (message "Delete is not available in this section")))))
(defun ssh-deploy-diff-mode--difference (parts)
"If file exists in both start a difference session based on PARTS."
(let ((section (nth 1 parts)))
(if (eq section 'in-both)
(let* ((file-name (nth 0 parts))
(root-local (file-truename (nth 2 parts)))
(root-remote (nth 3 parts))
(path-local (file-truename (expand-file-name file-name root-local)))
(path-remote (expand-file-name file-name root-remote)))
(ssh-deploy-diff-files path-local path-remote))
(message "File must exists in both roots to perform a difference action."))))
(defun ssh-deploy-diff-mode--open (parts)
"Perform a open file action based on PARTS from section A or section B."
(let* ((section (nth 1 parts))
(file-name (nth 0 parts))
(root-local (file-truename (nth 2 parts)))
(root-remote (nth 3 parts))
(path-local (file-truename (expand-file-name file-name root-local)))
(path-remote (expand-file-name file-name root-remote)))
(pcase section
('only-in-a
(message "Opening file '%s'" path-local)
(find-file path-local))
('only-in-b
(message "Opening file '%s'" path-remote)
(find-file path-remote))
(_ (message "Open is not available in this section")))))
(define-derived-mode ssh-deploy-diff-mode special-mode "SSH-Deploy-Diff"
"Major mode for SSH Deploy interactive directory differences."
(set (make-local-variable 'font-lock-defaults)
'(ssh-deploy-diff-mode--font-lock-keywords)))
(provide 'ssh-deploy-diff-mode)
;;; ssh-deploy-diff-mode.el ends here