working codeowners/who-owns
2 files changed, 199 insertions(+), 0 deletions(-)

A => codeowners-tests.el
A => codeowners.el
A => codeowners-tests.el +85 -0
@@ 0,0 1,85 @@ 
+(ert-deftest matches?/plain-matches ()
+  (should (codeowners//matches? "abc" "abc"))
+  (should (codeowners//matches? "abc" "fabc"))
+  (should (codeowners//matches? "abc" "abcd")))
+(ert-deftest matches?/base-folder-matches ()
+  (should (codeowners//matches? "foo" "/foo")))
+(ert-deftest matches?/folder-matches-at-end ()
+  (should (codeowners//matches? "foo" "/before/foo")))
+(ert-deftest matches?/folder-matches-partial ()
+  (should (codeowners//matches? "foo" "/before/food")))
+(ert-deftest matches?/folder-matches-in-middle ()
+  (should (codeowners//matches? "foo" "/before/foo/after")))
+(ert-deftest matches?/folder-matches-partial-in-middle ()
+  (should (codeowners//matches? "foo" "/before/afood/after")))
+(ert-deftest matches?/rooted-folder-against-middle ()
+  (should-not (codeowners//matches? "/foo" "/before/foo/after")))
+
+(ert-deftest matches?/slash-in-middle-is-rooted/success ()
+  (should (codeowners//matches? "a/b" "/a/b/c/d")))
+
+(ert-deftest matches?/slash-in-middle-is-rooted/fail ()
+  (should-not (codeowners//matches? "b/c" "/a/b/c/d")))
+
+(ert-deftest matches?/slash-at-end-doesnt-match-file ()
+  (should-not (codeowners//matches? "b/" "/a/b")))
+
+(ert-deftest matches?/slash-at-end-matches-directory ()
+  (should (codeowners//matches? "b/" "/a/b/")))
+
+(ert-deftest matches?/asterisk-at-end ()
+  (should (codeowners//matches? "c*" "/carlos")))
+
+(ert-deftest matches?/asterisk-in-middle ()
+  (should (codeowners//matches? "a*c" "/abbbbbc")))
+
+(ert-deftest matches?/asterisk-only ()
+  (should (codeowners//matches? "*" "/foo/bar/baz")))
+
+(ert-deftest matches?/asterisk-doesnt-match-slash ()
+  (should-not (codeowners//matches? "a*c" "/a/c")))
+
+(ert-deftest matches?/character-range ()
+  (should (codeowners//matches? "[a-z]" "/123a456")))
+
+(ert-deftest matches?/question-mark-only ()
+  (should (codeowners//matches? "?" "/foo/bar/baz")))
+
+(ert-deftest matches?/question-mark-doesnt-match-slash ()
+  (should-not (codeowners//matches? "a?c" "/a/c")))
+
+(ert-deftest matches?/two-asterisks-at-start-matches-at-start ()
+  (should (codeowners//matches? "**/foo" "/foo/bar")))
+
+(ert-deftest matches?/two-asterisks-at-start-matches-anywhere ()
+  (should (codeowners//matches? "**/bar" "/foo/bar")))
+
+(ert-deftest matches?/two-asterisks-at-start-matches-multiple-directories ()
+  (should (codeowners//matches? "**/baz/biff" "/foo/bar/baz/biff")))
+
+(ert-deftest matches?/two-asterisks-at-end-matches-everything ()
+  (should (codeowners//matches? "foo/**" "/foo/bar/baz")))
+
+(ert-deftest matches?/two-asterisks-at-end-is-rooted ()
+  (should-not (codeowners//matches? "bar/**" "/foo/bar")))
+
+(ert-deftest matches?/two-asterisks-in-middle-matches-zero-directories ()
+  (should (codeowners//matches? "foo/**/baz" "/foo/baz")))
+
+(ert-deftest matches?/two-asterisks-in-middle-matches-one-directory ()
+  (should (codeowners//matches? "foo/**/baz" "/foo/bar/baz")))
+
+(ert-deftest matches?/two-asterisks-in-middle-matches-two-directories ()
+  (should (codeowners//matches? "foo/**/biff" "/foo/bar/baz/biff")))
+
+(ert-deftest matches?/consecutive-sets-of-two-asterisks-still-work ()
+  (should (codeowners//matches? "foo/**/**/bar" "/foo/bar")))
+
+(ert-deftest matches?/two-asterisks-dont-match-mismatch ()
+  (should-not (codeowners//matches? "foo/**/bar" "/foo/baz")))
+
+(ert-deftest matches?/three-asterisks-is-one-asterisk/match ()
+  (should (codeowners//matches? "foo/***/bar" "/foo/anything/bar")))
+
+(ert-deftest matches?/three-asterisks-is-one-asterisk/doesnt-match-multiple-directories ()
+  (should-not (codeowners//matches? "foo/***/bar" "/foo/one/two/bar")))

          
A => codeowners.el +114 -0
@@ 0,0 1,114 @@ 
+;;; codeowners.el --- Work with codeowners files -*- lexical-binding: t; -*-
+
+;;; Package-Requires: ((emacs "25.1") (f "0.20.0") (dash "2.19.1"))
+
+;; Package-Version: 0.0.1
+
+;;; Homepage: https://zck.org/FIXME
+
+;;; Commentary:
+;; For seeing who owns a given file, use #'codeowners/who-owns.
+
+;;; Code:
+(require 'f)
+(require 'dash)
+
+;;zck do I need to add these to Package-Requires?
+(require 'cl-lib)
+(require 's)
+(require 'seq)
+
+(defun codeowners/who-owns (filename)
+  "Return who owns FILENAME.
+
+FILENAME is the path to a (possibly nonexistant) file."
+  (interactive "F")
+  (if-let ((owners (codeowners//who-owns-internal filename)))
+      (message "%s is owned by: %s"
+               filename
+               (string-join owners ", "))
+    (message "%s is not owned by anyone!.......yet!"
+             filename)))
+
+(defun codeowners//who-owns-internal (filename)
+  "Return who owns FILENAME.
+
+FILENAME is the path to a (possibly nonexistant) file.
+
+This is a helper function for /who-owns."
+  (if-let ((codeowners-file (codeowners//find-codeowners-file filename)))
+      (let* ((filename (expand-file-name filename))
+             (root-folder (file-name-directory codeowners-file))
+             (filename-relative-to-root (concat "/"
+                                                (string-remove-prefix root-folder filename)))
+             (codeowners-lines (codeowners//parse-codeowners-file codeowners-file)))
+        (--> codeowners-lines
+             (-last (lambda (codeowners-line)
+                      (let ((pattern (cl-first codeowners-line)))
+                        (codeowners//matches? pattern filename-relative-to-root)))
+                    it)
+             (cl-second it)))))
+
+(defun codeowners//parse-codeowners-file (filename)
+  "Parse the codeowners file at FILENAME.
+
+Each return value is a list where the first element is the pattern,
+and the second element is a list of owners."
+  (cl-loop for line in (split-string (f-read filename) "\n" t)
+           unless (string-prefix-p "#" line)
+           collect (let ((tokens (split-string line)))
+                     (list (cl-first tokens)
+                           (cl-rest tokens)))))
+
+(defun codeowners//matches? (pattern path)
+  "Return t if PATH is matched by PATTERN."
+      (setq pattern (replace-regexp-in-string "/\\*\\*/\\(\\*\\*/\\)+"
+                                              "/**/"
+                                              pattern))
+  (let* ((state 'consuming)
+         (regex "")
+         (slash-position (seq-position pattern ?/))
+         (is-absolute (and slash-position (< slash-position (1- (length pattern)))))
+         (chars (cl-coerce pattern 'list)))
+    (while chars
+      (let ((char (pop chars)))
+        (pcase state
+          ('consuming
+           (cl-case char
+             ((?*)
+              (setf state 'found-asterisk))
+             ((??)
+              (setf regex (format "%s[^/]" regex)))
+             (t (setf regex (format "%s%c" regex char)))))
+          ('found-asterisk
+           (setq state 'consuming)
+           (if (= char ?*)
+               (setf state 'two-asterisks)
+             (progn (setf regex (format "%s[^/]*%c" regex char)))))
+          ('two-asterisks
+           (if (= char ?/)
+               (setf regex (format "%s\\(.*/\\)*" regex))
+             (push ?* chars))
+           (setq state 'consuming)))))
+    (when is-absolute
+      (setf regex (concat "^/" regex)))
+    (string-match-p regex path)))
+
+(defun codeowners//find-codeowners-file (filename)
+  "Return the location of a codeowners file.
+
+This looks directories up from FILENAME, for a file named CODEOWNERS."
+  (let* ((filename (expand-file-name filename))
+         (directory-name (if (file-directory-p filename)
+                             filename
+                           (file-name-directory filename)))
+         (codeowners-candidate (concat directory-name "CODEOWNERS")))
+    (if (file-exists-p codeowners-candidate)
+        codeowners-candidate
+      ;;ugh, this way to find the parent directory is ugly.
+      ;;there's got to be a better way!
+      (unless (equal filename "/")
+        (codeowners//find-codeowners-file (file-name-directory (directory-file-name directory-name)))))))
+
+(provide 'codeowners)
+;;; codeowners.el ends here