Here's the latest version of my Source safe code. It is not vc based -- I prefer to have explicit control over things. The code does have a couple interesting features: there is an easy way to create private writable copies of a file, and later on merge those changes back into the real code (using ediff); it can automatically compose mail for you to send off notifying your teammates of the change. I don't claim that this code is well tested. It certainly isn't well documented, and there are a number of (small?) shortcomings. But we find it useful. You might, too. ======================================== ;;; -*- Mode: Emacs-Lisp -*- ;;; ;;; source-safe.el -- SourceSafe in Emacs ;;; (defconst ss-version "SS v1.0 beta2 Time-stamp: <96/12/10 10:14:36 lanning>") ;;; ;;; Copyright (C) 1996 by Pure Atria. ;;; ;;; Authors: ;;; Stan Lanning ;;; ============================================================ ;;; User definable variables (defvar ss-program "SS.exe" "*The SourceSafe executable") (defvar ss-project-dirs '() "*List associating pathnames with SourceSafe projects. Each item on the list is a pair of the form (DIR-REGEXP . PROJECT) where DIR-REGEXP is a regular expression that matches a directory, and PROJECT is the name of the SourceSafe project that matches that directory. For example, if you have your copy of the SourceSafe project $/MyProj in the directory D:\\MyProjDir, you would add the pair (\"^D:\\\\\\\\MyProjDir\\\\\\\" . \"$/MyProj/\") on this list.") (defvar ss-tmp-dir nil "Directory where Emacs can create SourceSafe temporary directories and files. If NIL, Emacs will create a temp directory at the root of your copy of the project tree.") (defvar ss-update-email-to nil "*If non-nil, a string specifying whom to send mail to when files are checked in.") (defvar ss-update-email-cc nil "*If non-nil, a string specifying who to cc mail to when files are checked in.") (defvar ss-update-email-subject "Checkin: >>subject<<" "*Default update mail subject") (defvar ss-update-email-body " Purpose of checkin: >>Global check-in message<< ======================================== " "*Default check-in mail body") (defvar ss-confirm-updates nil "*If true, confirm all UPDATE commands.") (defvar ss-trace nil "For debugging purposes. If true, don't actually do the SourceSafe commands, instead print what would be done.") (defvar ss-diff-ignore-whitespace t "*Should ss-diff ignore whitespace?") (defvar ss-update-new-frame nil "*If non-null, ss-update will open a new window (Emacs \"frame\") to edit the check-in message. The value of ss-update-new-frame will be passed to the function make-frame to construct the window.") ;;; ============================================================ ;;; Utility functions (defun ss-concat-dirname (dir file) "Concatenate the directory and file name into a full pathname. Handles the various cases of dir ending (or not ending) in a delimeter, and the file beginning (or not beginning) with a delimeter." (concat (directory-file-name dir) (make-string 1 directory-sep-char) (if (string-match "^/\\\\" file) (substring file 1) file))) (defun ss-set-file-writable-p (file writablep) (let ((cur-mode (file-modes file)) (writable-mask 146)) ;146 = 444 octal (set-file-modes file (if writablep (logior cur-mode writable-mask) (logand cur-mode (lognot writable-mask)))))) (defun ss-delete-file (f forcep) "Delete the file." (if forcep (ss-set-file-writable-p f t)) (delete-file f)) (defun ss-file-basename (f) "Strips of any leading directories and drives from the filename and returns the result." (if (string-match "[/\\\\:]\\([^/\\\\:]+\\)$" f) (substring f (match-beginning 1) (match-end 1)) f)) (defun ss-file-dirname (f) "Return the directory component of a pathname, without a trailing delimeter." (if (string-match "^\\(.*\\)[/\\\\:][^/\\\\:]+$" f) (substring f (match-beginning 1) (match-end 1)) ".")) (defun ss-tmp-dir (f) "Return a directory to use for temporary files related to the file F." (let (tmp-dir) (if (not (null ss-tmp-dir)) (setq tmp-dir ss-tmp-dir) ;; Try to use a temp dir at the root of the project dir (let ((projects ss-project-dirs)) (while (and projects (null tmp-dir)) (if (string-match (concat "^\\(" (car (car projects)) "\\)") f) (setq tmp-dir (substring f 0 (match-end 1))) (setq projects (cdr projects)))) (if (null tmp-dir) ;; That didn't work, so try a temp dir right here. (setq tmp-dir (ss-file-dirname f))))) (if (not (file-exists-p tmp-dir)) (make-directory tmp-dir)) (setq tmp-dir (ss-concat-dirname tmp-dir "emacstmp")) (if (not (file-exists-p tmp-dir)) (make-directory tmp-dir)) tmp-dir)) (defun ss-tmp-file (f) "Returns the full pathname of a temp file that does not currently exist. The temp file will have the same basename as the given file." (let ((basename (ss-file-basename f)) (tmp-dir-root (ss-tmp-dir f)) (count 0) tmp-dir tmpf) (while (progn (setq tmp-dir (ss-concat-dirname tmp-dir-root (format "%d" count))) (setq tmpf (ss-concat-dirname tmp-dir basename)) (or (file-exists-p tmpf) (and (file-exists-p tmp-dir) (not (file-directory-p tmp-dir))))) (setq count (1+ count))) (if (not (file-exists-p tmp-dir)) (make-directory tmp-dir)) (expand-file-name tmpf))) (defun ss-replace-all (s from to) "Replace all occurances in the string S of the regexp FROM to TO." (let (pos) (while (setq pos (string-match from s)) (setq s (concat (substring s 0 pos) to (substring s (1+ pos)))))) s) ;; "Baseline" files for branches and diffs (defconst ss-baseline-suffix "-baseline") (defun ss-baseline-name (f) (concat f ss-baseline-suffix)) (defun ss-make-baseline (f) (copy-file f (ss-baseline-name f) 0 t)) (defun ss-get-baseline (f) "Return the name of the basefile file for F if there is one, otherwise return NIL." (let ((base (ss-baseline-name f))) (if (and (file-exists-p base) (not (file-directory-p base))) base nil))) (defun ss-external-filename (f) "Converts a filename to a Windows filename." (ss-replace-all (expand-file-name f) "/" "\\")) (defun ss-project-filename (f) "Convert a filename into the SS project filename." (setq f (ss-external-filename f)) (catch 'converted (let ((projects ss-project-dirs)) (while projects (if (string-match (concat "^\\(" (car (car projects)) "\\)") f) (throw 'converted (concat (cdr (car projects)) (ss-replace-all (substring f (match-end 1)) "\\\\" "/")))) (setq projects (cdr projects)))))) (defvar ss-trace nil "For debugging purposes. If true, don't actually do the SourceSafe commands, instead print what would be done.") (defvar ss-quietly nil) (defun ss-log-buffer () (get-buffer-create "*SourceSafe log*")) (defun ss-do-command (cmd file infile &rest args) "*Execute the SourceSafe commamd CMD on source file FILE, with remaining args ARGS. Note that FILE is not the project name, but the real file name. Return T if the command succeeded, NIL if it failed gracefully. If the command failed ungracefully, raise an error." (if ss-trace (message "-- SS: cmd=%S file=%S args=%S\n" cmd file args) (let ((proc-buffer (ss-log-buffer)) (proj-file (ss-project-filename file)) status) (if (not ss-quietly) (message "Starting %s..." cmd)) (save-excursion (set-buffer proc-buffer) (erase-buffer)) (cond ((stringp proj-file) ) ((string-equal cmd "STATUS") (setq proj-file "$/")) (t (error "No project file found for %s" file))) (setq status (apply 'call-process ss-program infile proc-buffer t ;display? cmd proj-file args)) (cond ((zerop status) t) ((= status 1) nil) (t (error "SS failed: cmd=%S status=%S. See buffer \"%s\" for more info." cmd status (buffer-name proc-buffer))))))) (defun ss-get-current-version (f) "Get a copy of the currently checked in version of the file F, returning the name of the new file." (let ((new-file (ss-tmp-file f))) (ss-do-command "GET" f nil (concat "-GL" (ss-external-filename (ss-file-dirname new-file)))) (if (not (file-exists-p new-file)) (error "SS GET failed: %S not created" new-file) new-file))) (defun ss-current-owner (f) "Return the name of the person who currently has the file F checked out. If the file is not locked, return FALSE" (if (ss-do-command "STATUS" f nil) ;; True means not currently checked out. Go figure. nil ;; False means is checked out. Parse the output buffer to get owner. (save-excursion (set-buffer (ss-log-buffer)) (goto-line 2) (if (looking-at (concat (regexp-quote (ss-file-basename f)) " *\\([^ ]+\\)")) (downcase (buffer-substring (match-beginning 1) (match-end 1))) (error "Can't determine current file owner"))))) (defun ss-project-file-exists-p (f) "Return TRUE iff the file exists in SourceSafe" (let ((existsp t)) (condition-case c (ss-do-command "STATUS" f nil) (error (setq existsp nil))) existsp)) (defun ss-file-modified-p (f) "Is the file modified (not the same as the currently checked in version)?" (let ((ss-quietly t)) (not (ss-do-command "DIFF" f nil "-B")))) (defun ss-file-checked-out-p (f) "Return true iff the file is currently checked out by us." ;; Currently a really hackish implementation. (and (file-writable-p f) (not (ss-get-baseline f)))) (defun ss-verify-test (forcep fmt &rest args) (or (and (integerp forcep) (yes-or-no-p (apply 'format fmt args))) (and (not (integerp forcep)) (not (null forcep))))) (defun ss-verify-overwrite (f &optional forcep) "Verify that the user wants to overwrite the file. If the optional FORCEP is nil, and the file has been modified, don't do it. If FORCEP is an integer, and the file has been modified, ask before clobbering it. Otherwise, just lose any changes without asking." (or (not (ss-file-modified-p f)) (and (integerp forcep) (yes-or-no-p (format "The file %s has been modified. Discard the changes? " f))) (and (not (integerp forcep)) (not (null forcep))))) (defvar ss-checkin-mail-buffer nil) (defun ss-build-checkin-message (buf comments-file) "Build up a SourceSafe check-in mail message." (if ss-update-email-to (let ((pop-up-windows t) (special-display-buffer-names nil) (special-display-regexps nil) (same-window-buffer-names nil) (same-window-regexps nil) (buf-name "*SS checkin mail*")) (require 'sendmail) (pop-to-buffer buf-name) ;; Make sure the SS checkin message buffer is initialized (if (not (equal ss-checkin-mail-buffer (current-buffer))) (progn (mail-mode) (mail-setup ss-update-email-to ss-update-email-subject nil ;in-reply-to ss-update-email-cc nil ;replybuffer '(ss-checkin-message-sent)) (goto-char (point-max)) (insert ss-update-email-body))) (setq ss-checkin-mail-buffer (current-buffer)) ;; Add info about this checkin (goto-char (point-max)) (insert "\n" (ss-project-filename (buffer-file-name buf)) "\n") (let ((comment-start (point))) (insert-file comments-file) (goto-char (point-max)) (insert "\n") (indent-rigidly comment-start (point) 4))))) (defun ss-checkin-message-sent () "The SS checkin message has just been sent." (if (buffer-live-p ss-checkin-mail-buffer) (save-excursion (set-buffer ss-checkin-mail-buffer) (rename-buffer "*SS checkin mail (sent)*" t) (setq ss-checkin-mail-buffer nil)))) (defun ss-make-ediff-cleanup (form) `(lambda () (make-local-variable 'ediff-cleanup-hook) (setq ediff-cleanup-hook (cons (lambda () ,form) ediff-cleanup-hook)))) ;;; ============================================================ ;;; Public interface functions ;;; ###autoload (defun ss-diff () (interactive) "*Run ediff on the current buffer, comparing it to the current version of the file under SourceSafe." (require 'ediff) (save-buffer) (let ((ss-file (ss-get-current-version (buffer-file-name))) ss-buffer (ediff-ignore-similar-regions (or ss-diff-ignore-whitespace ediff-ignore-similar-regions))) ;; Rename the gotten version so the user ;; can make sense of the files in ediff. (let ((new-name (concat ss-file "-diff" ss-baseline-suffix))) (if (file-exists-p new-name) (ss-delete-file new-name t)) (rename-file ss-file new-name) (setq ss-file new-name)) ;; Run ediff (find-file-noselect ss-file) (setq ss-buffer (get-file-buffer ss-file)) (ediff-buffers (current-buffer) ss-buffer (list (ss-make-ediff-cleanup `(condition-case c (progn (kill-buffer ',ss-buffer) (ss-delete-file ',ss-file t)) (error nil))))))) ;;; ###autoload (defun ss-get () (interactive) "*Get the latest version of the file currently being visited." (cond ((and (eq major-mode 'dired-mode) (ss-verify-test 0 "Really update the entire tree %s? " dired-directory)) (save-excursion (pop-to-buffer (ss-log-buffer))) (if (unwind-protect (ss-get-directory dired-directory t) (revert-buffer t t)) (message "SS GET complete.") (message "SS GET failed.") (beep))) ((and (not (eq major-mode 'dired-mode)) (progn (save-buffer) (ss-get-file (buffer-file-name) 0))) (revert-buffer t t) (message "SS GET complete. If you want to get the lock on the file, use ss-checkout.")) (t (message "SS GET canceled.") (beep)))) (defun ss-get-file (f &optional forcep) "*Get the latest version of the file F. Return true iff the GET succeeded." (cond ((ss-verify-overwrite f forcep) (ss-set-file-writable-p f nil) (ss-do-command "GET" f nil) t) (t nil))) (defun ss-get-directory (d &optional forcep) "*Get the latest version of all the files in directory D. Return true iff the GET succeeded." (cond ((ss-verify-test forcep "Really update the entire tree %s? " d) (ss-do-command "GET" d nil "-R" "-I-N") t) (t nil))) ;;; ###autoload (defun ss-checkout () (interactive) "*Check out the currently visited file so you can edit it." (save-buffer) (cond ((ss-checkout-file (buffer-file-name) 0) (revert-buffer t t) (message "SS CHECKOUT complete. Use ss-update to check it back in.")) (t (message "SS CHECKOUT canceled.") (beep)))) (defun ss-checkout-file (f &optional forcep) "*Check out the file F so you can edit it." (let ((cur-owner (ss-current-owner f))) (cond ((equal (user-real-login-name) cur-owner) (error "You already have the file locked!")) (cur-owner (error "File is currently locked by %s." cur-owner)) ((ss-verify-overwrite f forcep) (ss-do-command "CHECKOUT" f nil) t) (t nil)))) ;;; ###autoload (defun ss-uncheckout () (interactive) "*Un-checkout the currently visited file. Any changes made to the file will be lost." (save-buffer) (cond ((ss-uncheckout-file (buffer-file-name) 0) (revert-buffer t t) (message "SS UNCHECKOUT complete.")) (t (message "SS UNCHECKOUT canceled.") (beep)))) (defun ss-uncheckout-file (f &optional forcep) "*Un-checkout the file F. If the optional FORCEP is nil, and the file has been modified, don't do it. If FORCEP is an integer, and the file has been modified, ask before clobbering it. Otherwise, just lose any changes without asking." (cond ((not (equal (user-real-login-name) (ss-current-owner f))) (error "You don't own the file!")) ((ss-verify-overwrite f forcep) (let ((tmpfile (ss-tmp-file "ssyes.txt"))) (save-excursion (find-file tmpfile) (erase-buffer) (insert "y\n") (save-buffer) (kill-buffer (current-buffer))) (ss-do-command "UNCHECKOUT" f tmpfile) (ss-delete-file tmpfile t) t)) (t nil))) ;;; ###autoload (defun ss-update () (interactive) "*Check in the currently visted file." (let ((orig-buffer (current-buffer)) (orig-windows (current-window-configuration)) (orig-frames (current-frame-configuration)) (bufname "*SourceSafe update comment*") (fname (buffer-file-name (current-buffer)))) ;; Before we get carried away, make sure everything is kosher (cond ((not (ss-project-file-exists-p fname)) (if (not (y-or-n-p "File not in the SS project. Do you want to create it there? ")) (error "File not found in SourceSafe"))) ((not (equal (user-real-login-name) (ss-current-owner fname))) (error "You don't own the file!")) ) ;; Now get carried away. (save-buffer) (cond ((null ss-update-new-frame) (switch-to-buffer-other-window (get-buffer-create bufname))) ((listp ss-update-new-frame) (select-frame (make-frame ss-update-new-frame)) (switch-to-buffer (get-buffer-create bufname))) (t (select-frame (make-frame)) (switch-to-buffer (get-buffer-create bufname)))) (if (and ss-buffer-in-use (not (yes-or-no-p "Update comment buffer in use: steal it? "))) (switch-to-buffer (generate-new-buffer bufname))) ;; Associate some info with the buffer so we can finish ;; with the update when the user is done with the comment. (setq ss-original-buffer orig-buffer ss-original-window-config orig-windows ss-original-frame-config orig-frames ss-buffer-in-use t) ;; Set up ^C-^C to finish the comments. (if (null ss-get-update-message-mode-map) (progn (setq ss-get-update-message-mode-map (make-sparse-keymap)) (define-key ss-get-update-message-mode-map "\C-c\C-c" 'ss-update-finish))) (use-local-map ss-get-update-message-mode-map) (setq major-mode 'ss-get-update-message-mode) (setq mode-name "SS Update message") (setq buffer-read-only nil) (message "Enter check-in message. Hit ^C-^C when done; kill the buffer to quit."))) (defvar ss-get-update-message-mode-map nil) (make-variable-buffer-local 'ss-buffer-in-use) (make-variable-buffer-local 'ss-original-buffer) (make-variable-buffer-local 'ss-original-window-config) (make-variable-buffer-local 'ss-original-frame-config) (setq-default ss-buffer-in-use nil ss-original-buffer nil ss-original-window-config nil ss-original-frame-config nil ) (defun ss-update-finish () (interactive) (let* ((comment-file (ss-tmp-file "update.txt")) (orig-buffer ss-original-buffer) (orig-windows ss-original-window-config) (orig-frames ss-original-frame-config) (cmd (if (ss-project-file-exists-p (buffer-file-name orig-buffer)) "UPDATE" "ADD"))) (if (or (not ss-confirm-updates) (yes-or-no-p (concat "Really update file " (buffer-file-name orig-buffer) "? "))) (progn (write-region (point-min) (point-max) comment-file nil t) (setq ss-buffer-in-use nil) (set-buffer orig-buffer) (ss-do-command cmd (buffer-file-name) nil (concat "-C@" (ss-external-filename comment-file))) (set-frame-configuration orig-frames) (set-window-configuration orig-windows) (revert-buffer t t) (ss-build-checkin-message orig-buffer comment-file) (message (format "SS %s complete." cmd))) (set-frame-configuration orig-frames) (set-window-configuration orig-windows) (set-buffer orig-buffer) (message (format "SS %s aborted." cmd)) (beep)))) ;;; ###autoload (defun ss-branch () (interactive) "*Branch off a private, writable copy of the current file for you to work on. You can merge this back in later on by using the function (ss-merge)." (save-buffer) (cond ((and (ss-file-checked-out-p (buffer-file-name)) (not (yes-or-no-p "File currently checked out. Convert to a private branch? "))) (message "SS BRANCH aborted.") (beep)) ((ss-branch-file (buffer-file-name)) (revert-buffer t t) (message "SS BRANCH complete. Use (ss-merge) to merge back in your changes, (ss-unbranch) to revert.")) (t (message "SS BRANCH failed.") (beep)))) (defun ss-branch-file (f) (cond ((equal (user-real-login-name) (ss-current-owner f)) (error "Can't branch -- you already have it locked!")) ((ss-file-checked-out-p f) ;; Convert changes to a private branch. (let ((tmp (concat f "@TMP"))) (copy-file f tmp) (ss-uncheckout-file f t) (rename-file f (ss-baseline-name f)) (rename-file tmp f) t)) (t (ss-make-baseline f) (ss-set-file-writable-p f t) t))) ;;; ###autoload (defun ss-unbranch () (interactive) "*Delete a private branch of the current file. This is not undoable." (cond ((ss-unbranch-file (buffer-file-name) 0 (yes-or-no-p "Get latest copy of file? ")) (revert-buffer t t) (message "SS UNBRANCH complete.")) (t (message "SS UNBRANCH canceled.") (beep)))) (defun ss-unbranch-file (f &optional forcep getp) "*Delete a private branch of the current file. This is not undoable." (let ((basef (ss-get-baseline f))) (cond ((not basef) (error "No branch found for %s" f)) ((not (ss-verify-overwrite f forcep)) ;; Abort nil) (getp ;; Toss the branch, update to latest version (ss-delete-file basef t) (ss-get-file f t) t) (t ;; Toss the branch, put the baseline back (ss-delete-file f t) (rename-file basef f) t)))) ;;; ###autoload (defun ss-merge () (interactive) "*Check out the current file and merge in the changes that you have made. See the function ss-branch." (require 'ediff) (save-buffer) (if (buffer-file-name) (let* ((file (buffer-file-name)) (modified (concat file "-modified")) (baseline (ss-get-baseline file)) (cleanup (ss-make-ediff-cleanup `(ss-merge-cleanup ',file ',modified ',baseline)))) (save-buffer) (ss-set-file-writable-p file t) (condition-case c (progn (message "Moving your file to %s..." modified) (rename-file file modified) (if (ss-checkout-file file nil);(ss-do-command "CHECKOUT" file nil) (condition-case c (progn (revert-buffer t t) (message "Starting merge...") (cond ((not baseline) ;; Two-way merge (ediff-merge-files file modified (list cleanup))) ((and (ss-compare-files file baseline) (y-or-n-p "No changes checked in since the branch. Accept all your changes? ")) ;; No change since split, so just take the modified version. (ss-delete-file file t) (rename-file modified file)) (t ;; Do the three-way merge. (ediff-merge-files-with-ancestor file modified baseline (list cleanup))));<<< WHERE IS OUTPUT? t) (error (message "Error %s. Reverting to modified file." (car c)) (ss-uncheckout) (error "%s" (car c)))) (error "Can't check out file %s." file))) (error (message "Cleaning up from aborted merge - %s." c) (if (file-exists-p file) (delete-file file)) (if (file-exists-p modified) (rename-file modified file t)) (ss-set-file-writable-p file t) (revert-buffer t t) nil))) (error "There is no file associated with buffer %s" (buffer-name)))) (defun ss-compare-files (f1 f2) "Compre files f1 and f2. Return true iff they are identical." ;; NOT YET IMPLEMENTED nil) (defun ss-merge-cleanup (file modified baseline) (let ((merge-buffer (ediff-get-buffer (ediff-char-to-buftype ?c))) (filebuf (find-buffer-visiting file))) (cond ((not (buffer-live-p merge-buffer)) (message "Could not find merge buffer -- you are on your own.")) ((yes-or-no-p (format "Are you happy with the buffer %S? " (buffer-name merge-buffer))) ;; Save the merge (ss-delete-file file t) (save-excursion (set-buffer merge-buffer) (write-region (point-min) (point-max) file) (if filebuf (progn (set-buffer filebuf) (revert-buffer t t)))) ;; Delete temp files and buffers (if (and baseline (yes-or-no-p (concat "Delete the original baseline file " baseline "? "))) (let (buf) (ss-delete-file baseline t) (while (setq buf (find-buffer-visiting baseline)) (kill-buffer buf)))) (if (yes-or-no-p (concat "Delete copy of the modified " modified "? ")) (let (buf) (ss-delete-file modified t) (while (setq buf (find-buffer-visiting modified)) (kill-buffer buf)))) ;; All done (message "SS MERGE complete. Don't forget to CHECKIN the file.")) ((yes-or-no-p "Abort the merge, and uncheckout the file? ") ;; Abort the merge (ss-uncheckout-file file t) (ss-delete-file file t) (rename-file modified file) nil) (t ;; Merge OK, but user didn't want it saved. (message "OK, it's up to you to save the merge...") (beep))))) ;;; ###autoload (defun ss-history () (interactive) "*Show the checkin history of the currently visited file." (let ((f (buffer-file-name)) (tmpfile (ss-tmp-file "sshist.txt"))) (ss-do-command "HISTORY" f nil (concat "-O@" (ss-external-filename tmpfile))) (if (file-exists-p tmpfile) (progn (pop-to-buffer "*SS History*") (erase-buffer) (insert-file tmpfile) (goto-char (point-min)))))) ;;; ###autoload (defun ss-status () (interactive) "*Show the status of the current file." (let ((f (buffer-file-name)) (tmpfile (ss-tmp-file "ssstatus.txt"))) (ss-do-command "STATUS" f nil (concat "-O@" (ss-external-filename tmpfile))) (if (file-exists-p tmpfile) (progn (pop-to-buffer "*SS Status*") (erase-buffer) (insert-file tmpfile) (goto-char (point-min)))))) ;;; ###autoload (defun ss-help () (interactive) "*Describe the SourceSafe mode" (pop-to-buffer "*Help*") (erase-buffer) (insert "The source-safe package is an interface to SourceSafe from Emacs. The package defines these top-level interactive functions: (ss-get) Get the latest version of the file currently being visited. If run in a dired buffer, updates the entire directory and any sub-directories. (ss-checkout) Check out the currently visited file so you can edit it. (ss-uncheckout) Un-checkout the curently visited file. (ss-update) Check in the currently visited file. (ss-diff) Run ediff on the current buffer, comparing it to the current version of the file under SourceSafe. This uses ediff, not SourceSafe diff. (ss-branch) Branch off a private, writable copy of the current file for you to work on. You can merge this back in later on by using the function (ss-merge). This makes a note of the current baseline file, so merging can use a more intelligent three-way merge. If the current file is already checked out, converts the file to a private branch and unchecks out the file. (ss-unbranch) Deletes a private branch, abandoning all changes you have made. (ss-merge) Check out the current file and merge in the changes that you have made. See the function (ss-branch). (ss-history) Display the SourceSafe checkin history for the current file in a buffer. (ss-help) Print out this help text. There are a couple of variables of interest: ss-program The pathname of the SourceSafe program. Default is \"SS.exe\". ss-project-dirs Specifies how to convert NT file names to project names. The value is a list of pairs of the form (FILENAME-REGEXP . PROJECT) To create the project filename from a Windows filename, the list is searched for the first FILENAME-REGEXP that matches the filename. The part of the filename that matched is then replaced by the PROJECT, and finally all backslashes are converted to forward slashes. For example, you might set ss-project-dirs like this: (setq ss-project-dirs '((\"^C:\\\\\\\\MyProjDir\\\\\\\\\" . \"$/MyProj/\") (\"^D:\\\\\\\\MyProjDir\\\\\\\\\" . \"$/MyProj/\") (\"^E:\\\\\\\\MyProjDir\\\\\\\\\" . \"$/MyProj/\") (\"^C:\\\\\\\\MyProj\\\\\\\\\" . \"$/MyProj/\") (\"^D:\\\\\\\\MyProj\\\\\\\\\" . \"$/MyProj/\") (\"^E:\\\\\\\\MyProj\\\\\\\\\" . \"$/MyProj/\"))) Beware! Since those are regexp patterns, you need to have lots of \\ characters in there. ss-update-email-to If not NULL, when you checkin a file (or should I say \"update\"\?), emacs automatically creates a mail message addressed to the value of this variable, indicating the file name and the checkin message. Subsequent checkins add their info to the message. You might set ss-update-email-to like this: (setq ss-update-email-to \"my-proj-eng\") ss-update-email-cc Like ss-update-email-to, but the default CC: list for mail. ss-update-emacs-subject Default text for the subject line for update email messages. ss-update-emacs-body Default initial text for the body of update email messages. ss-diff-ignore-whitespace If TRUE (the default), ss-diff will ignore trivial differences like whitespace. ss-update-new-frame If a list, ss-update will open a new window (\"frame\" in Emacs terminology) to edit the checkin message, using the value as the parameters to make-frame. If T, open a new frame with the default make-frame parameters. If NULL, edit the checkin message in the existing frame. This can be very useful if you follow ss-update by a call to ss-diff: you can then step through the changes to the file as a guide to constructing the checkin message. ss-tmp-dir The name of a directory where Emacs and SourceSafe can create directories for temporary files. If NIL (the default), emacs will create a directory at the root of your project tree. Since SourceSafe sometimes has problems dealing with FAT file systems, the default is most certainly what you want. " ) (goto-char (point-min))) (provide 'source-safe)