diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/coverage-core.el | 64 |
1 files changed, 64 insertions, 0 deletions
diff --git a/modules/coverage-core.el b/modules/coverage-core.el index 4a3112ce..a2b03817 100644 --- a/modules/coverage-core.el +++ b/modules/coverage-core.el @@ -54,5 +54,69 @@ Signals `user-error' if FILE does not exist." (forward-line 1))) result)) +(defconst cj/--coverage-hunk-header-regexp + "^@@ -[0-9]+\\(,[0-9]+\\)? \\+\\([0-9]+\\)\\(,\\([0-9]+\\)\\)? @@" + "Regexp for a git unified-diff hunk header. +Captures new_start (group 2) and new_count (group 4; nil implies 1).") + +(defconst cj/--coverage-file-marker-regexp + "^\\+\\+\\+ b/\\(.+\\)$" + "Regexp for the \"+++ b/<path>\" line of a git diff. +Captures the file path (group 1).") + +(defun cj/--coverage-parse-diff-output (output) + "Parse OUTPUT, a git unified-diff string, into a hash table. +Keys are file paths (relative to repo root, as git emits them). Values +are hash tables whose keys are line numbers added or modified in the new +version of the file. A file that appears with only deletions maps to an +empty hash table. Malformed hunk headers are skipped silently." + (let ((result (make-hash-table :test 'equal)) + (current-lines nil)) + (with-temp-buffer + (insert output) + (goto-char (point-min)) + (while (not (eobp)) + (let ((line (buffer-substring-no-properties + (line-beginning-position) (line-end-position)))) + (cond + ((string-match cj/--coverage-file-marker-regexp line) + (let ((path (match-string 1 line))) + (setq current-lines (make-hash-table :test 'eql)) + (puthash path current-lines result))) + ((string-prefix-p "+++ /dev/null" line) + (setq current-lines nil)) + ((and current-lines + (string-match cj/--coverage-hunk-header-regexp line)) + (let* ((new-start (string-to-number (match-string 2 line))) + (count-str (match-string 4 line)) + (new-count (if count-str + (string-to-number count-str) + 1))) + (when (> new-count 0) + (dotimes (i new-count) + (puthash (+ new-start i) t current-lines))))))) + (forward-line 1))) + result)) + +(defun cj/--coverage-changed-lines (scope &optional base) + "Return a hash table of files to changed line numbers for SCOPE. +SCOPE is one of the symbols `working-tree', `staged', `branch-vs-main', +or `branch-vs-parent'. For `branch-vs-parent', BASE is the ref to +compare against; if nil, falls back to the tracked upstream @{upstream}. +Signals `user-error' for any other SCOPE." + (let ((cmd (cond + ((eq scope 'working-tree) + "git diff HEAD --unified=0") + ((eq scope 'staged) + "git diff --cached --unified=0") + ((eq scope 'branch-vs-main) + "git diff $(git merge-base HEAD main)..HEAD --unified=0") + ((eq scope 'branch-vs-parent) + (format "git diff $(git merge-base HEAD %s)..HEAD --unified=0" + (or base "@{upstream}"))) + (t + (user-error "Unknown coverage scope: %s" scope))))) + (cj/--coverage-parse-diff-output (shell-command-to-string cmd)))) + (provide 'coverage-core) ;;; coverage-core.el ends here |
