[[[
Fix issue #2357: Commit and diff are not recursive if files have been marked.

Suggested by: Joshua Varner <jlvarner@gmail.com>

* contrib/client-side/psvn/psvn.el
  (svn-status-recursive-commit): New variable.
  (svn-status-files-to-commit): Refer to it in the docstring.
  (svn-process-sentinel): When clearing svn-status-files-to-commit,
    clear svn-status-recursive-commit as well.
  (svn-status-some-files-marked-p): New function.
  (svn-status-commit-file): Is recursive only if there are no marks.
    Set svn-status-recursive-commit accordingly.
  (svn-log-edit-done): Respect svn-status-recursive-commit.
  (svn-log-edit-show-files-to-commit, svn-log-edit-insert-files-to-commit):
    Tell the user if the commit is going to be recursive.

  (svn-status-show-svn-diff-internal): Major rewrite with an
    incompatible interface.  This function now ignores marks; the
    caller must provide the list of files and a recursion flag.  The
    HEAD/BASE selection logic works independently for each file, and
    callers can specify arbitrary revisions as strings, although the
    rest of the code doesn't currently use these features.
  (svn-status-show-svn-diff): Adapted to the new interface.  Is always
    recursive.
  (svn-status-show-svn-diff-for-marked-files): Adapted to the new
    interface.  Is recursive only if there are no marks.
  (svn-prop-edit-svn-diff, svn-log-edit-svn-diff): Show diffs for the
    files being propset or committed, rather than the currently marked
    files.  The diff is recursive if the original command is.

  (svn-status-show-svn-diff): Wrapped docstring to 80 columns.
]]]

--- psvn.el	(revision 15394)
+++ psvn.el	(revision 15394 variant kon.1.recursive)
@@ -300,7 +300,12 @@ (defvar svn-status-default-revision-widt
 (defvar svn-status-default-author-width 9)
 (defvar svn-status-line-format " %c%c%c %4s %4s %-9s")
 (defvar svn-start-of-file-list-line-number 0)
-(defvar svn-status-files-to-commit nil)
+(defvar svn-status-files-to-commit nil
+  "List of files to commit at `svn-log-edit-done'.
+This is always set together with `svn-status-recursive-commit'.")
+(defvar svn-status-recursive-commit nil
+  "Non-nil if the next commit should be recursive.
+This is always set together with `svn-status-files-to-commit'.")
 (defvar svn-status-pre-commit-window-configuration nil)
 (defvar svn-status-pre-propedit-window-configuration nil)
 (defvar svn-status-head-revision nil)
@@ -672,7 +677,8 @@ (defun svn-process-sentinel (process eve
                     (svn-status-unset-all-usermarks))
                   (svn-status-update-with-command-list (svn-status-parse-commit-output))
                   (run-hooks 'svn-log-edit-done-hook)
-                  (setq svn-status-files-to-commit nil)
+                  (setq svn-status-files-to-commit nil
+                        svn-status-recursive-commit nil)
                   (message "svn commit finished"))
                  ((eq svn-process-cmd 'update)
                   (svn-status-show-process-output 'update t)
@@ -2057,6 +2063,15 @@ (defun svn-status-marked-files ()
 (defun svn-status-marked-file-names ()
   (mapcar 'svn-status-line-info->filename (svn-status-marked-files)))
 
+(defun svn-status-some-files-marked-p ()
+  "Return non-nil iff a file has been marked by `svn-status-set-user-mark'.
+Unlike `svn-status-marked-files', this does not select the file under point
+if no files have been marked."
+  ;; `some' would be shorter but requires cl-seq at runtime.
+  ;; (Because it accepts both lists and vectors, it is difficult to inline.)
+  (loop for file in svn-status-info
+        thereis (svn-status-line-info->has-usermark file)))
+
 (defun svn-status-ui-information-hash-table ()
   (let ((st-info svn-status-info)
         (svn-status-ui-information (make-hash-table :test 'equal)))
@@ -2156,34 +2171,49 @@ (defun svn-status-blame ()
 
 (defun svn-status-show-svn-diff (arg)
   "Run `svn diff' on the current file.
-If there is a newer revision in the repository, the diff is done against HEAD, otherwise
-compare the working copy with BASE.
+If the current file is a directory, compare it recursively.
+If there is a newer revision in the repository, the diff is done against HEAD,
+otherwise compare the working copy with BASE.
 If ARG then prompt for revision to diff against."
   (interactive "P")
   (svn-status-ensure-cursor-on-file)
-  (svn-status-show-svn-diff-internal arg nil))
+  (svn-status-show-svn-diff-internal (list (svn-status-get-line-information)) t
+                                     (if arg :ask :auto)))
 
 (defun svn-status-show-svn-diff-for-marked-files (arg)
   "Run `svn diff' on all selected files.
-See `svn-status-marked-files' for what counts as selected.
+If some files have been marked, compare those non-recursively;
+this is because marking a directory with \\[svn-status-set-user-mark]
+normally marks all of its files as well.
+If no files have been marked, compare recursively the file at point.
 If ARG then prompt for revision to diff against, else compare working copy with BASE."
   (interactive "P")
-  (svn-status-show-svn-diff-internal arg t))
-
-(defun svn-status-show-svn-diff-internal (arg &optional use-all-marked-files)
-  (let* ((fl (if use-all-marked-files
-                 (svn-status-marked-files)
-               (list (svn-status-get-line-information))))
-         (clear-buf t)
-         (revision (if arg
-                       (svn-status-read-revision-string "Diff with files for version: " "PREV")
-                     (if use-all-marked-files
-                         "BASE"
-                       (if (svn-status-line-info->update-available (car fl)) "HEAD" "BASE")))))
-    (while fl
-      (svn-run-svn nil clear-buf 'diff "diff" "-r" revision (svn-status-line-info->filename (car fl)))
-      (setq clear-buf nil)
-      (setq fl (cdr fl))))
+  (svn-status-show-svn-diff-internal (svn-status-marked-files)
+                                     (not (svn-status-some-files-marked-p))
+                                     (if arg :ask "BASE")))
+
+(defun svn-status-show-svn-diff-internal (files recursive revision)
+  ;; REVISION must be one of:
+  ;; - a string: whatever the -r option allows.
+  ;; - `:ask': asks the user to specify the revision, which then becomes
+  ;;   saved in `minibuffer-history' rather than in `command-history'.
+  ;; - `:auto': use "HEAD" if an update is known to exist, "BASE" otherwise.
+  ;; In the future, `nil' might mean omit the -r option entirely;
+  ;; but that currently seems to imply "BASE", so we just use that.
+  (when (eq revision :ask)
+    (setq revision (svn-status-read-revision-string
+                    "Diff with files for version: " "PREV")))
+  (let ((clear-buf t))
+    (dolist (file files)
+      (apply #'svn-run-svn nil clear-buf 'diff
+             `("diff"
+               "-r" ,(if (eq revision :auto)
+                         (if (svn-status-line-info->update-available file)
+                             "HEAD" "BASE")
+                       revision)
+               ,@(unless recursive '("--non-recursive"))
+               ,(svn-status-line-info->filename file)))
+      (setq clear-buf nil)))
   (svn-status-diff-mode))
 
 (defun svn-status-diff-mode ()
@@ -2356,10 +2386,15 @@ (defun svn-status-update-cmd ()
 
 (defun svn-status-commit-file ()
   "Commit selected files.
-See `svn-status-marked-files' for what counts as selected."
-  (interactive)
-  (let* ((marked-files (svn-status-marked-files)))
-    (setq svn-status-files-to-commit marked-files)
+If some files have been marked, commit those non-recursively;
+this is because marking a directory with \\[svn-status-set-user-mark]
+normally marks all of its files as well.
+If no files have been marked, commit recursively the file at point."
+  (interactive)
+  (let* ((selected-files (svn-status-marked-files))
+         (marked-files-p (svn-status-some-files-marked-p)))
+    (setq svn-status-files-to-commit selected-files
+          svn-status-recursive-commit (not marked-files-p))
     (svn-log-edit-show-files-to-commit)
     (svn-status-pop-to-commit-buffer)
     (when svn-log-edit-insert-files-to-commit
@@ -2889,7 +2924,9 @@ (defun svn-prop-edit-do-it (async)
 (defun svn-prop-edit-svn-diff (arg)
   (interactive "P")
   (set-buffer svn-status-buffer-name)
-  (svn-status-show-svn-diff-for-marked-files arg))
+  ;; Because propedit is not recursive in our use, neither is this diff.
+  (svn-status-show-svn-diff-internal svn-status-propedit-file-list nil
+                                     (if arg :ask "BASE")))
 
 (defun svn-prop-edit-svn-log (arg)
   (interactive "P")
@@ -2988,8 +3025,11 @@ (defun svn-log-edit-done ()
                  (string= "." (svn-status-line-info->filename (car svn-status-files-to-commit)))))
       (svn-status-create-arg-file svn-status-temp-arg-file ""
                                   svn-status-files-to-commit "")
-      (svn-run-svn t t 'commit "commit" "--targets" svn-status-temp-arg-file
-                   "-F" svn-status-temp-file-to-remove))
+      (apply #'svn-run-svn t t 'commit
+             `("commit"
+               ,@(unless svn-status-recursive-commit '("--non-recursive"))
+               "--targets" ,svn-status-temp-arg-file
+               "-F" ,svn-status-temp-file-to-remove)))
     (set-window-configuration svn-status-pre-commit-window-configuration)
     (message "svn-log editing done")))
 
@@ -2997,8 +3037,12 @@ (defun svn-log-edit-svn-diff (arg)
   "Show the diff we are about to commit.
 If ARG then show diff between some other version of the selected files."
   (interactive "P")
-  (set-buffer svn-status-buffer-name)
-  (svn-status-show-svn-diff-for-marked-files arg))
+  (set-buffer svn-status-buffer-name)   ; TODO: is this necessary?
+  ;; This call is very much like `svn-status-show-svn-diff-for-marked-files'
+  ;; but uses commit-specific variables instead of the current marks.
+  (svn-status-show-svn-diff-internal svn-status-files-to-commit
+                                     svn-status-recursive-commit
+                                     (if arg :ask "BASE")))
 
 (defun svn-log-edit-svn-log (arg)
   (interactive "P")
@@ -3015,7 +3059,9 @@ (defun svn-log-edit-files-to-commit ()
 
 (defun svn-log-edit-show-files-to-commit ()
   (interactive)
-  (message "Files to commit: %S" (svn-log-edit-files-to-commit)))
+  (message "Files to commit%s: %S"
+           (if svn-status-recursive-commit " recursively" "")
+           (svn-log-edit-files-to-commit)))
 
 (defun svn-log-edit-save-message ()
   "Save the current log message to the file `svn-log-edit-file-name'."
@@ -3035,7 +3081,8 @@ (defun svn-log-edit-insert-files-to-comm
     (save-excursion
       (goto-char (point-min))
       (insert "## Lines starting with '## ' will be removed from the log message.\n")
-      (insert "## File(s) to commit:\n")
+      (insert "## File(s) to commit"
+              (if svn-status-recursive-commit " recursively" "") ":\n")
       (let ((file-list svn-status-files-to-commit))
         (while file-list
           (insert (concat "## " (svn-status-line-info->filename (car file-list)) "\n"))

