From 812499df69067491d3ac0636ddb784a49d58a9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 11:57:05 +0100 Subject: [PATCH 01/36] Initial commit --- .gitignore | 3 ++ magit-standup.el | 135 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 .gitignore create mode 100644 magit-standup.el diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e2a636 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.elc +.cask/ +dist/ diff --git a/magit-standup.el b/magit-standup.el new file mode 100644 index 0000000..ae3907f --- /dev/null +++ b/magit-standup.el @@ -0,0 +1,135 @@ +;;; magit-standup.el --- Collect recent git commits for standup notes -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Function Artisans, Ltd. + +;; Author: István Karaszi +;; Version: 0.1.0 +;; Package-Requires: ((emacs "27.1") (magit "4.5.0")) +;; Keywords: tools, vc +;; URL: https://github.com/function-artisans/magit-standup + +;; 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 of the License, 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 this program. If not, see . + +;;; Commentary: + +;; Collect recent git commits across multiple repositories and format +;; them as org-mode standup notes. On Mondays it automatically looks +;; back to Friday; otherwise it looks back to the previous day. The +;; list of repositories and the lookback behavior are configurable. +;; +;; Usage: +;; M-x magit-standup +;; +;; Customize `magit-standup-repos' to specify which repositories to +;; scan. When nil, only the current repository is used. + +;;; Code: + +(require 'magit) + +(defgroup magit-standup nil + "Collect recent git commits for standup notes." + :group 'magit + :prefix "magit-standup-") + +(defcustom magit-standup-repos nil + "List of directory paths to collect commits from. +When nil, only the current repository is used." + :type '(repeat directory) + :group 'magit-standup) + +(defcustom magit-standup-author nil + "Author name or email to filter commits by. +When nil, the result of `git config user.name' is used." + :type '(choice (const :tag "From git config" nil) + (string :tag "Author name/email")) + :group 'magit-standup) + +(defcustom magit-standup-since-days-ago nil + "Override for how many days back to look. +When nil, automatic weekday-aware logic is used: on Monday look +back to Friday (3 days), otherwise look back 1 day." + :type '(choice (const :tag "Automatic" nil) + (integer :tag "Days ago")) + :group 'magit-standup) + +(defun magit-standup--since-date () + "Return the \"since\" date string for filtering commits. +If `magit-standup-since-days-ago' is set, use it. Otherwise, if +today is Monday use last Friday; else use yesterday." + (let* ((day (string-to-number (format-time-string "%u" (current-time)))) + (days-ago (cond (magit-standup-since-days-ago magit-standup-since-days-ago) + ((<= 6 day) (- day 5)) + ((= 1 day) 3) + (t 1)))) + (format-time-string "%Y-%m-%d" + (time-subtract (current-time) + (days-to-time days-ago))))) + +(defun magit-standup--collect-commits (repo-path since-date author) + "Collect commits from REPO-PATH since SINCE-DATE by AUTHOR. +Returns a list of commit message strings." + (let ((default-directory (file-name-as-directory repo-path))) + (magit-git-lines "log" "--oneline" "--all" "--reflog" + (concat "--after=" since-date) + (concat "--author=" author)))) + +(defun magit-standup--format-org (repo-commits) + "Format REPO-COMMITS as `org-mode' text. +REPO-COMMITS is an alist of (REPO-NAME . COMMITS) where each +COMMITS is a list of commit message strings." + (mapconcat + (lambda (entry) + (let ((repo-name (car entry)) + (commits (cdr entry))) + (if commits + (concat "* " repo-name "\n" + (mapconcat (lambda (c) (concat "- " c)) commits "\n") + "\n") + (concat "* " repo-name "\n- (no commits)\n")))) + repo-commits + "\n")) + +;;;###autoload +(defun magit-standup () + "Display recent git commits as `org-mode' standup notes. +Collects commits from all repos in `magit-standup-repos' (or the +current repo if that is nil) and displays them in a +`*magit-standup*' buffer." + (interactive) + (let* ((since-date (magit-standup--since-date)) + (author (or magit-standup-author + (magit-git-string "config" "user.email"))) + (repos (or magit-standup-repos + (list (magit-toplevel)))) + (repo-commits + (mapcar (lambda (repo) + (cons (file-name-nondirectory + (directory-file-name repo)) + (magit-standup--collect-commits repo since-date author))) + repos)) + (buf (get-buffer-create "*magit-standup*"))) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (magit-standup--format-org repo-commits))) + (goto-char (point-min)) + (org-mode)) + (pop-to-buffer buf))) + +(provide 'magit-standup) + +;;; magit-standup.el ends here From ccaecd0bd3c603e6e60e47498a5b75da31b3605e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 11:59:52 +0100 Subject: [PATCH 02/36] Fix linter warning --- .dir-locals.el | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .dir-locals.el diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..918ed07 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,6 @@ +((emacs-lisp-mode + . ((eval . (progn + (require 'package) + (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) + (unless (assoc "magit" package-archive-contents) + (package-refresh-contents))))))) From 2c09b20d95063b14d88d12e4c06494fe9a714273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:27:42 +0100 Subject: [PATCH 03/36] Add tests --- Easkfile | 18 +++++++++ test/magit-standup-test.el | 78 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 Easkfile create mode 100644 test/magit-standup-test.el diff --git a/Easkfile b/Easkfile new file mode 100644 index 0000000..bc3230b --- /dev/null +++ b/Easkfile @@ -0,0 +1,18 @@ +(package "magit-standup" + "0.1.0" + "Collect recent git commits for standup notes") + +(website-url "https://github.com/function-artisans/magit-standup") +(keywords "tools" "vc") + +(package-file "magit-standup.el") + +(source "gnu") +(source "melpa") + +(depends-on "emacs" "27.1") +(depends-on "magit" "4.5.0") + +(development + (depends-on "package-lint") + (depends-on "buttercup")) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el new file mode 100644 index 0000000..eb3b7ff --- /dev/null +++ b/test/magit-standup-test.el @@ -0,0 +1,78 @@ +;;; magit-standup-test.el --- Tests for magit-standup -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Buttercup tests for magit-standup. + +;;; Code: + +(require 'buttercup) +(require 'magit-standup) + +(describe "magit-standup--since-date" + (it "looks back 3 days on Monday (to Friday)" + ;; Monday 2026-01-05 12:00:00 UTC — %u = 1 + (let ((magit-standup-since-days-ago nil)) + (cl-letf (((symbol-function 'current-time) + (lambda () (encode-time 0 0 12 5 1 2026)))) + (expect (magit-standup--since-date) :to-equal "2026-01-02")))) + + (it "looks back 1 day on Tuesday" + ;; Tuesday 2026-01-06 12:00:00 UTC — %u = 2 + (let ((magit-standup-since-days-ago nil)) + (cl-letf (((symbol-function 'current-time) + (lambda () (encode-time 0 0 12 6 1 2026)))) + (expect (magit-standup--since-date) :to-equal "2026-01-05")))) + + (it "looks back 1 day on Wednesday" + ;; Wednesday 2026-01-07 12:00:00 UTC — %u = 3 + (let ((magit-standup-since-days-ago nil)) + (cl-letf (((symbol-function 'current-time) + (lambda () (encode-time 0 0 12 7 1 2026)))) + (expect (magit-standup--since-date) :to-equal "2026-01-06")))) + + (it "looks back to Friday on Saturday (1 day)" + ;; Saturday 2026-01-10 12:00:00 UTC — %u = 6 + (let ((magit-standup-since-days-ago nil)) + (cl-letf (((symbol-function 'current-time) + (lambda () (encode-time 0 0 12 10 1 2026)))) + (expect (magit-standup--since-date) :to-equal "2026-01-09")))) + + (it "looks back to Friday on Sunday (2 days)" + ;; Sunday 2026-01-11 12:00:00 UTC — %u = 7 + (let ((magit-standup-since-days-ago nil)) + (cl-letf (((symbol-function 'current-time) + (lambda () (encode-time 0 0 12 11 1 2026)))) + (expect (magit-standup--since-date) :to-equal "2026-01-09")))) + + (it "uses custom override when magit-standup-since-days-ago is set" + ;; Wednesday 2026-01-07 — would normally be 1 day back + (let ((magit-standup-since-days-ago 7)) + (cl-letf (((symbol-function 'current-time) + (lambda () (encode-time 0 0 12 7 1 2026)))) + (expect (magit-standup--since-date) :to-equal "2025-12-31"))))) + +(describe "magit-standup--format-org" + (it "formats repo with commits as org headings" + (expect (magit-standup--format-org + '(("my-repo" . ("abc123 Fix bug" + "def456 Add feature")))) + :to-equal + "* my-repo\n- abc123 Fix bug\n- def456 Add feature\n")) + + (it "shows placeholder when repo has no commits" + (expect (magit-standup--format-org + '(("empty-repo"))) + :to-equal + "* empty-repo\n- (no commits)\n")) + + (it "separates multiple repos with blank lines" + (expect (magit-standup--format-org + '(("repo-a" . ("abc Fix thing")) + ("repo-b" . ("def Other thing")))) + :to-equal + (concat "* repo-a\n- abc Fix thing\n" + "\n" + "* repo-b\n- def Other thing\n")))) + +;;; magit-standup-test.el ends here From 757cc8dd5c90a1d0760d89dc825e024c6edc0b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:27:50 +0100 Subject: [PATCH 04/36] Add a GitHub CI setup --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d961602 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + emacs_version: ['27.1', '28.2', '29.4', '30.2'] + steps: + - uses: actions/checkout@v4 + - uses: jcs090218/setup-emacs@master + with: + version: ${{ matrix.emacs_version }} + - uses: emacs-eask/setup-eask@master + with: + version: 'snapshot' + - run: eask package + - run: eask install-deps + - run: eask compile + - run: eask lint package + - run: eask test buttercup From e79f8aa9cb583c3dfa1fd4b5ecfda196b5f7ee7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:32:55 +0100 Subject: [PATCH 05/36] Ignore .eask --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6e2a636..c52583c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.elc +.eask/ .cask/ dist/ From 25bd3ff9442d95d68ca0117e5e086d313e7c842f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:33:56 +0100 Subject: [PATCH 06/36] Bump up the minimum Emacs version --- .github/workflows/ci.yml | 2 +- Easkfile | 2 +- magit-standup.el | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d961602..94e5dd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - emacs_version: ['27.1', '28.2', '29.4', '30.2'] + emacs_version: ['28.1', '29.4', '30.2'] steps: - uses: actions/checkout@v4 - uses: jcs090218/setup-emacs@master diff --git a/Easkfile b/Easkfile index bc3230b..f157275 100644 --- a/Easkfile +++ b/Easkfile @@ -10,7 +10,7 @@ (source "gnu") (source "melpa") -(depends-on "emacs" "27.1") +(depends-on "emacs" "28.1") (depends-on "magit" "4.5.0") (development diff --git a/magit-standup.el b/magit-standup.el index ae3907f..6c351bf 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -4,7 +4,7 @@ ;; Author: István Karaszi ;; Version: 0.1.0 -;; Package-Requires: ((emacs "27.1") (magit "4.5.0")) +;; Package-Requires: ((emacs "28.1") (magit "4.5.0")) ;; Keywords: tools, vc ;; URL: https://github.com/function-artisans/magit-standup From af80ad6e07cd766e0a1397ac2a36a7d00e1c65a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:34:39 +0100 Subject: [PATCH 07/36] Fix comment --- magit-standup.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magit-standup.el b/magit-standup.el index 6c351bf..becc633 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -53,7 +53,7 @@ When nil, only the current repository is used." (defcustom magit-standup-author nil "Author name or email to filter commits by. -When nil, the result of `git config user.name' is used." +When nil, the result of `git config user.email' is used." :type '(choice (const :tag "From git config" nil) (string :tag "Author name/email")) :group 'magit-standup) From 1527ab66cf16234c2cf47471eb9acbc983ae2525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:40:36 +0100 Subject: [PATCH 08/36] Add branch names between commits --- magit-standup.el | 35 ++++++++++++++++++++++++----------- test/magit-standup-test.el | 35 +++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index becc633..6e06fbc 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -81,24 +81,37 @@ today is Monday use last Friday; else use yesterday." (defun magit-standup--collect-commits (repo-path since-date author) "Collect commits from REPO-PATH since SINCE-DATE by AUTHOR. -Returns a list of commit message strings." +Returns an alist of (BRANCH-NAME . COMMITS) where COMMITS is a +list of commit message strings." (let ((default-directory (file-name-as-directory repo-path))) - (magit-git-lines "log" "--oneline" "--all" "--reflog" - (concat "--after=" since-date) - (concat "--author=" author)))) + (let ((branches (magit-git-lines "branch" "--format=%(refname:short)"))) + (mapcar (lambda (branch) + (cons branch + (magit-git-lines "log" "--oneline" + (concat "--after=" since-date) + (concat "--author=" author) + branch))) + branches)))) (defun magit-standup--format-org (repo-commits) "Format REPO-COMMITS as `org-mode' text. -REPO-COMMITS is an alist of (REPO-NAME . COMMITS) where each -COMMITS is a list of commit message strings." +REPO-COMMITS is an alist of (REPO-NAME . BRANCH-COMMITS) where +BRANCH-COMMITS is an alist of (BRANCH-NAME . COMMITS)." (mapconcat (lambda (entry) - (let ((repo-name (car entry)) - (commits (cdr entry))) - (if commits + (let* ((repo-name (car entry)) + (branch-commits (cdr entry)) + (active (seq-filter #'cdr branch-commits))) + (if active (concat "* " repo-name "\n" - (mapconcat (lambda (c) (concat "- " c)) commits "\n") - "\n") + (mapconcat + (lambda (bc) + (concat "** ~" (car bc) "~\n" + (mapconcat (lambda (c) (concat "- " c)) + (cdr bc) "\n") + "\n")) + active + "\n")) (concat "* " repo-name "\n- (no commits)\n")))) repo-commits "\n")) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index eb3b7ff..1ece169 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -53,26 +53,41 @@ (expect (magit-standup--since-date) :to-equal "2025-12-31"))))) (describe "magit-standup--format-org" - (it "formats repo with commits as org headings" + (it "formats branch commits with subheadings" (expect (magit-standup--format-org - '(("my-repo" . ("abc123 Fix bug" - "def456 Add feature")))) + '(("my-repo" . (("main" . ("abc123 Fix bug" + "def456 Add feature")))))) :to-equal - "* my-repo\n- abc123 Fix bug\n- def456 Add feature\n")) + "* my-repo\n** ~main~\n- abc123 Fix bug\n- def456 Add feature\n")) - (it "shows placeholder when repo has no commits" + (it "shows placeholder when all branches have no commits" (expect (magit-standup--format-org - '(("empty-repo"))) + '(("empty-repo" . (("main") ("develop"))))) :to-equal "* empty-repo\n- (no commits)\n")) + (it "skips branches with no commits" + (expect (magit-standup--format-org + '(("my-repo" . (("main" . ("abc Fix thing")) + ("stale-branch"))))) + :to-equal + "* my-repo\n** ~main~\n- abc Fix thing\n")) + + (it "shows multiple branches under one repo" + (expect (magit-standup--format-org + '(("my-repo" . (("main" . ("abc Fix thing")) + ("feature" . ("def Add thing")))))) + :to-equal + (concat "* my-repo\n** ~main~\n- abc Fix thing\n" + "\n** ~feature~\n- def Add thing\n"))) + (it "separates multiple repos with blank lines" (expect (magit-standup--format-org - '(("repo-a" . ("abc Fix thing")) - ("repo-b" . ("def Other thing")))) + '(("repo-a" . (("main" . ("abc Fix thing")))) + ("repo-b" . (("develop" . ("def Other thing")))))) :to-equal - (concat "* repo-a\n- abc Fix thing\n" + (concat "* repo-a\n** ~main~\n- abc Fix thing\n" "\n" - "* repo-b\n- def Other thing\n")))) + "* repo-b\n** ~develop~\n- def Other thing\n")))) ;;; magit-standup-test.el ends here From 2b7ab2650166babefe0ff427b0b62f93457d5e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:42:20 +0100 Subject: [PATCH 09/36] Evaluate current-time once --- magit-standup.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 6e06fbc..152ade9 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -70,13 +70,14 @@ back to Friday (3 days), otherwise look back 1 day." "Return the \"since\" date string for filtering commits. If `magit-standup-since-days-ago' is set, use it. Otherwise, if today is Monday use last Friday; else use yesterday." - (let* ((day (string-to-number (format-time-string "%u" (current-time)))) + (let* ((now (current-time)) + (day (string-to-number (format-time-string "%u" now))) (days-ago (cond (magit-standup-since-days-ago magit-standup-since-days-ago) ((<= 6 day) (- day 5)) ((= 1 day) 3) (t 1)))) (format-time-string "%Y-%m-%d" - (time-subtract (current-time) + (time-subtract now (days-to-time days-ago))))) (defun magit-standup--collect-commits (repo-path since-date author) From 7d73a2dbdcb92befb8f05c6e91b3d270850ebd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:43:13 +0100 Subject: [PATCH 10/36] Combine to let* --- magit-standup.el | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 152ade9..c1774d3 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -84,15 +84,15 @@ today is Monday use last Friday; else use yesterday." "Collect commits from REPO-PATH since SINCE-DATE by AUTHOR. Returns an alist of (BRANCH-NAME . COMMITS) where COMMITS is a list of commit message strings." - (let ((default-directory (file-name-as-directory repo-path))) - (let ((branches (magit-git-lines "branch" "--format=%(refname:short)"))) - (mapcar (lambda (branch) - (cons branch - (magit-git-lines "log" "--oneline" - (concat "--after=" since-date) - (concat "--author=" author) - branch))) - branches)))) + (let* ((default-directory (file-name-as-directory repo-path)) + (branches (magit-git-lines "branch" "--format=%(refname:short)"))) + (mapcar (lambda (branch) + (cons branch + (magit-git-lines "log" "--oneline" + (concat "--after=" since-date) + (concat "--author=" author) + branch))) + branches))) (defun magit-standup--format-org (repo-commits) "Format REPO-COMMITS as `org-mode' text. From 719cee5c3bee7be855eb005bf3263fc16d8c0eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:44:36 +0100 Subject: [PATCH 11/36] Add missing require --- magit-standup.el | 1 + 1 file changed, 1 insertion(+) diff --git a/magit-standup.el b/magit-standup.el index c1774d3..10a655e 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -39,6 +39,7 @@ ;;; Code: (require 'magit) +(require 'seq) (defgroup magit-standup nil "Collect recent git commits for standup notes." From f9137100895d1d9962c4a89872457d06a891eff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 12:45:16 +0100 Subject: [PATCH 12/36] Add error if author could not be determined --- magit-standup.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/magit-standup.el b/magit-standup.el index 10a655e..76ca20e 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -127,7 +127,8 @@ current repo if that is nil) and displays them in a (interactive) (let* ((since-date (magit-standup--since-date)) (author (or magit-standup-author - (magit-git-string "config" "user.email"))) + (magit-git-string "config" "user.email") + (user-error "Cannot determine author; set `magit-standup-author' or git config user.email"))) (repos (or magit-standup-repos (list (magit-toplevel)))) (repo-commits From b76178b9b6453e9eac5c2c4fb9612477a3342f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 14:04:51 +0100 Subject: [PATCH 13/36] Refactor magit-standup --- magit-standup.el | 29 +++++++++++--------- test/magit-standup-test.el | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 76ca20e..cd849b6 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -118,6 +118,22 @@ BRANCH-COMMITS is an alist of (BRANCH-NAME . COMMITS)." repo-commits "\n")) +(defun magit-standup--gather () + "Gather recent commits across all configured repositories. +Returns an alist of (REPO-NAME . BRANCH-COMMITS) suitable for +`magit-standup--format-org'." + (let* ((since-date (magit-standup--since-date)) + (author (or magit-standup-author + (magit-git-string "config" "user.email") + (user-error "Cannot determine author; set `magit-standup-author' or git config user.email"))) + (repos (or magit-standup-repos + (list (magit-toplevel))))) + (mapcar (lambda (repo) + (cons (file-name-nondirectory + (directory-file-name repo)) + (magit-standup--collect-commits repo since-date author))) + repos))) + ;;;###autoload (defun magit-standup () "Display recent git commits as `org-mode' standup notes. @@ -125,18 +141,7 @@ Collects commits from all repos in `magit-standup-repos' (or the current repo if that is nil) and displays them in a `*magit-standup*' buffer." (interactive) - (let* ((since-date (magit-standup--since-date)) - (author (or magit-standup-author - (magit-git-string "config" "user.email") - (user-error "Cannot determine author; set `magit-standup-author' or git config user.email"))) - (repos (or magit-standup-repos - (list (magit-toplevel)))) - (repo-commits - (mapcar (lambda (repo) - (cons (file-name-nondirectory - (directory-file-name repo)) - (magit-standup--collect-commits repo since-date author))) - repos)) + (let* ((repo-commits (magit-standup--gather)) (buf (get-buffer-create "*magit-standup*"))) (with-current-buffer buf (let ((inhibit-read-only t)) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index 1ece169..5071207 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -52,6 +52,18 @@ (lambda () (encode-time 0 0 12 7 1 2026)))) (expect (magit-standup--since-date) :to-equal "2025-12-31"))))) +(describe "magit-standup--collect-commits" + (it "sets default-directory to the repo path" + (let (captured-dirs) + (spy-on 'magit-git-lines :and-call-fake + (lambda (&rest _) + (push default-directory captured-dirs) + nil)) + (magit-standup--collect-commits "/tmp/my-repo" "2026-01-05" "alice") + (expect captured-dirs :not :to-be nil) + (dolist (dir captured-dirs) + (expect dir :to-equal "/tmp/my-repo/"))))) + (describe "magit-standup--format-org" (it "formats branch commits with subheadings" (expect (magit-standup--format-org @@ -90,4 +102,46 @@ "\n" "* repo-b\n** ~develop~\n- def Other thing\n")))) +(describe "magit-standup--gather" + (before-each + (spy-on 'magit-standup--since-date :and-return-value "2026-01-05") + (spy-on 'magit-standup--collect-commits :and-return-value + '(("main" . ("abc Fix thing"))))) + + (it "uses magit-standup-author when set" + (let ((magit-standup-repos '("/tmp/repo"))) + (spy-on 'magit-git-string) + (let ((magit-standup-author "alice")) + (magit-standup--gather)) + (expect 'magit-standup--collect-commits + :to-have-been-called-with "/tmp/repo" "2026-01-05" "alice") + (expect 'magit-git-string :not :to-have-been-called))) + + (it "falls back to git config user.email" + (let ((magit-standup-repos '("/tmp/repo")) + (magit-standup-author nil)) + (spy-on 'magit-git-string :and-return-value "bob@example.com") + (magit-standup--gather) + (expect 'magit-standup--collect-commits + :to-have-been-called-with "/tmp/repo" "2026-01-05" "bob@example.com"))) + + (it "signals error when no author can be determined" + (let ((magit-standup-repos '("/tmp/repo")) + (magit-standup-author nil)) + (spy-on 'magit-git-string :and-return-value nil) + (expect (magit-standup--gather) :to-throw 'user-error))) + + (it "uses magit-standup-repos when set" + (let ((magit-standup-repos '("/tmp/a" "/tmp/b")) + (magit-standup-author "alice")) + (let ((result (magit-standup--gather))) + (expect (mapcar #'car result) :to-equal '("a" "b"))))) + + (it "falls back to magit-toplevel when repos is nil" + (let ((magit-standup-repos nil) + (magit-standup-author "alice")) + (spy-on 'magit-toplevel :and-return-value "/home/user/my-project") + (let ((result (magit-standup--gather))) + (expect (mapcar #'car result) :to-equal '("my-project")))))) + ;;; magit-standup-test.el ends here From d21cc89b4acbbdf76ff47145e8bb0acb5917377d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 14:25:48 +0100 Subject: [PATCH 14/36] Collect directories recursively --- magit-standup.el | 38 +++++++++++++++++++-- test/magit-standup-test.el | 68 +++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index cd849b6..b1a741d 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -48,10 +48,19 @@ (defcustom magit-standup-repos nil "List of directory paths to collect commits from. -When nil, only the current repository is used." +When nil, only the current repository is used. Entries that are +not git repositories are searched recursively for nested repos, +up to `magit-standup-repos-max-depth' levels deep." :type '(repeat directory) :group 'magit-standup) +(defcustom magit-standup-repos-max-depth 1 + "Maximum depth to search for git repositories in non-repo directories. +When nil, search with unlimited depth." + :type '(choice (const :tag "Unlimited" nil) + (integer :tag "Max depth")) + :group 'magit-standup) + (defcustom magit-standup-author nil "Author name or email to filter commits by. When nil, the result of `git config user.email' is used." @@ -81,6 +90,31 @@ today is Monday use last Friday; else use yesterday." (time-subtract now (days-to-time days-ago))))) +(defun magit-standup--git-repo-p (dir) + "Return non-nil if DIR contains a `.git' directory or file." + (file-directory-p (expand-file-name ".git" dir))) + +(defun magit-standup--find-repos (dir depth) + "Recursively find git repositories under DIR up to DEPTH levels. +When DEPTH is nil, search with unlimited depth. Hidden +directories are skipped." + (cond + ((magit-standup--git-repo-p dir) (list dir)) + ((and depth (<= depth 0)) nil) + (t (mapcan (lambda (child) + (when (and (file-directory-p child) + (not (string-prefix-p "." (file-name-nondirectory child)))) + (magit-standup--find-repos child (and depth (1- depth))))) + (directory-files dir t nil t))))) + +(defun magit-standup--resolve-repos (dirs) + "Expand DIRS to a list of git repository paths. +Entries that are already git repos are kept as-is. Others are +searched recursively up to `magit-standup-repos-max-depth'." + (mapcan (lambda (dir) + (magit-standup--find-repos dir magit-standup-repos-max-depth)) + dirs)) + (defun magit-standup--collect-commits (repo-path since-date author) "Collect commits from REPO-PATH since SINCE-DATE by AUTHOR. Returns an alist of (BRANCH-NAME . COMMITS) where COMMITS is a @@ -126,7 +160,7 @@ Returns an alist of (REPO-NAME . BRANCH-COMMITS) suitable for (author (or magit-standup-author (magit-git-string "config" "user.email") (user-error "Cannot determine author; set `magit-standup-author' or git config user.email"))) - (repos (or magit-standup-repos + (repos (or (magit-standup--resolve-repos magit-standup-repos) (list (magit-toplevel))))) (mapcar (lambda (repo) (cons (file-name-nondirectory diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index 5071207..9aaf64a 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -64,6 +64,71 @@ (dolist (dir captured-dirs) (expect dir :to-equal "/tmp/my-repo/"))))) +(describe "magit-standup--resolve-repos" + :var (tmpdir) + + (before-each + (setq tmpdir (make-temp-file "standup-test-" t)) + ;; Create a git repo at tmpdir/repo-a + (let ((repo-a (expand-file-name "repo-a" tmpdir))) + (make-directory repo-a) + (make-directory (expand-file-name ".git" repo-a))) + ;; Create a git repo at tmpdir/repo-b + (let ((repo-b (expand-file-name "repo-b" tmpdir))) + (make-directory repo-b) + (make-directory (expand-file-name ".git" repo-b))) + ;; Create a non-repo dir with a nested repo at tmpdir/parent/nested + (let ((nested (expand-file-name "parent/nested" tmpdir))) + (make-directory nested t) + (make-directory (expand-file-name ".git" nested))) + ;; Create a hidden dir with a repo inside (should be skipped) + (let ((hidden (expand-file-name ".hidden/secret-repo" tmpdir))) + (make-directory hidden t) + (make-directory (expand-file-name ".git" hidden)))) + + (after-each + (delete-directory tmpdir t)) + + (it "returns nil for nil input" + (let ((magit-standup-repos-max-depth 1)) + (expect (magit-standup--resolve-repos nil) :to-be nil))) + + (it "returns a git repo directory as-is" + (let ((magit-standup-repos-max-depth 1) + (repo-a (expand-file-name "repo-a" tmpdir))) + (expect (magit-standup--resolve-repos (list repo-a)) + :to-equal (list repo-a)))) + + (it "discovers repos in immediate subdirectories" + (let ((magit-standup-repos-max-depth 1)) + (let ((result (sort (magit-standup--resolve-repos (list tmpdir)) #'string<))) + (expect result :to-equal + (sort (list (expand-file-name "repo-a" tmpdir) + (expand-file-name "repo-b" tmpdir)) + #'string<))))) + + (it "discovers nested repos with sufficient depth" + (let ((magit-standup-repos-max-depth 2)) + (let ((result (sort (magit-standup--resolve-repos (list tmpdir)) #'string<))) + (expect result :to-contain + (expand-file-name "parent/nested" tmpdir))))) + + (it "stops at depth 0" + (let ((magit-standup-repos-max-depth 0)) + (expect (magit-standup--resolve-repos (list tmpdir)) :to-be nil))) + + (it "searches unlimited depth when max-depth is nil" + (let ((magit-standup-repos-max-depth nil)) + (let ((result (magit-standup--resolve-repos (list tmpdir)))) + (expect result :to-contain + (expand-file-name "parent/nested" tmpdir))))) + + (it "skips hidden directories" + (let ((magit-standup-repos-max-depth nil)) + (let ((result (magit-standup--resolve-repos (list tmpdir)))) + (expect result :not :to-contain + (expand-file-name ".hidden/secret-repo" tmpdir)))))) + (describe "magit-standup--format-org" (it "formats branch commits with subheadings" (expect (magit-standup--format-org @@ -106,7 +171,8 @@ (before-each (spy-on 'magit-standup--since-date :and-return-value "2026-01-05") (spy-on 'magit-standup--collect-commits :and-return-value - '(("main" . ("abc Fix thing"))))) + '(("main" . ("abc Fix thing")))) + (spy-on 'magit-standup--resolve-repos :and-call-fake #'identity)) (it "uses magit-standup-author when set" (let ((magit-standup-repos '("/tmp/repo"))) From 12591cbfe3ce54f016fa2ca6ad8de593ba0066c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 14:30:19 +0100 Subject: [PATCH 15/36] Add a helper Makefile --- .gitignore | 2 ++ Makefile | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index c52583c..271496d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.elc +.deps +.compile .eask/ .cask/ dist/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..83c5dc8 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.PHONY: lint test clean + +.deps: Easkfile + eask install-deps + @touch .deps + +.compile: .deps magit-standup.el + eask compile + @touch .compile + +lint: .deps + eask lint package + +test: .compile + eask test buttercup + +clean: + eask clean all + @rm -f .deps .compile From 3f3af7a37c4890f66bca4a4fc6dcfa8c05664fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 23:44:10 +0100 Subject: [PATCH 16/36] Add links to the commits --- magit-standup.el | 77 ++++++++++++++++++++++++++++++------- test/magit-standup-test.el | 78 ++++++++++++++++++++++++++++++++------ 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index b1a741d..075bd95 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -68,6 +68,18 @@ When nil, the result of `git config user.email' is used." (string :tag "Author name/email")) :group 'magit-standup) +(defcustom magit-standup-link-package nil + "Package to use for linking commit hashes in org output. +When nil, auto-detect by checking if `orgit' or `org-git-link' +is loaded. When `orgit', use orgit-rev links. When +`org-git-link', use git links. When `none', plain text +without links." + :type '(choice (const :tag "Auto-detect" nil) + (const :tag "orgit" orgit) + (const :tag "org-git-link" org-git-link) + (const :tag "Plain text" none)) + :group 'magit-standup) + (defcustom magit-standup-since-days-ago nil "Override for how many days back to look. When nil, automatic weekday-aware logic is used: on Monday look @@ -115,27 +127,61 @@ searched recursively up to `magit-standup-repos-max-depth'." (magit-standup--find-repos dir magit-standup-repos-max-depth)) dirs)) +(defun magit-standup--detect-link-package () + "Detect which git-link package is available. +Returns `orgit' if orgit is loaded, `org-git-link' if +org-git-link is loaded, or nil if neither is available." + (cond + ((featurep 'orgit) 'orgit) + ((featurep 'org-git-link) 'org-git-link) + (t nil))) + +(defun magit-standup--link-prefix (package) + "Return the org link type string for PACKAGE. +PACKAGE should be `orgit', `org-git-link', or nil." + (pcase package + ('orgit "orgit-rev") + ('org-git-link "git") + (_ nil))) + +(defun magit-standup--format-commit (repo-path line &optional link-prefix) + "Format a commit LINE, optionally as an org link. +REPO-PATH is the repository directory. LINE is expected to have +the hash separated from the rest by a null byte. LINK-PREFIX is +the org link prefix string, or nil for plain text." + (let* ((parts (split-string line "\0")) + (hash (car parts)) + (rest (cadr parts))) + (if link-prefix + (concat "[[" link-prefix ":" repo-path "::" hash "][" hash "]]" + " " rest) + (concat hash " " rest)))) + (defun magit-standup--collect-commits (repo-path since-date author) "Collect commits from REPO-PATH since SINCE-DATE by AUTHOR. Returns an alist of (BRANCH-NAME . COMMITS) where COMMITS is a -list of commit message strings." +list of raw commit strings with hash and message separated by a +null byte." (let* ((default-directory (file-name-as-directory repo-path)) (branches (magit-git-lines "branch" "--format=%(refname:short)"))) (mapcar (lambda (branch) (cons branch - (magit-git-lines "log" "--oneline" + (magit-git-lines "log" "--format=%h%x00%s <%ai> - %aN" (concat "--after=" since-date) (concat "--author=" author) branch))) branches))) -(defun magit-standup--format-org (repo-commits) +(defun magit-standup--format-org (repo-commits &optional link-prefix) "Format REPO-COMMITS as `org-mode' text. -REPO-COMMITS is an alist of (REPO-NAME . BRANCH-COMMITS) where -BRANCH-COMMITS is an alist of (BRANCH-NAME . COMMITS)." +REPO-COMMITS is an alist of (REPO-PATH . BRANCH-COMMITS) where +BRANCH-COMMITS is an alist of (BRANCH-NAME . COMMITS). +LINK-PREFIX is the org link prefix string, or nil for plain text." (mapconcat (lambda (entry) - (let* ((repo-name (car entry)) + (let* ((repo-path (car entry)) + (repo-name (file-name-nondirectory + (directory-file-name repo-path))) (branch-commits (cdr entry)) (active (seq-filter #'cdr branch-commits))) (if active @@ -143,8 +189,11 @@ BRANCH-COMMITS is an alist of (BRANCH-NAME . COMMITS)." (mapconcat (lambda (bc) (concat "** ~" (car bc) "~\n" - (mapconcat (lambda (c) (concat "- " c)) - (cdr bc) "\n") + (mapconcat + (lambda (c) + (concat "- " (magit-standup--format-commit + repo-path c link-prefix))) + (cdr bc) "\n") "\n")) active "\n")) @@ -154,7 +203,7 @@ BRANCH-COMMITS is an alist of (BRANCH-NAME . COMMITS)." (defun magit-standup--gather () "Gather recent commits across all configured repositories. -Returns an alist of (REPO-NAME . BRANCH-COMMITS) suitable for +Returns an alist of (REPO-PATH . BRANCH-COMMITS) suitable for `magit-standup--format-org'." (let* ((since-date (magit-standup--since-date)) (author (or magit-standup-author @@ -163,8 +212,7 @@ Returns an alist of (REPO-NAME . BRANCH-COMMITS) suitable for (repos (or (magit-standup--resolve-repos magit-standup-repos) (list (magit-toplevel))))) (mapcar (lambda (repo) - (cons (file-name-nondirectory - (directory-file-name repo)) + (cons repo (magit-standup--collect-commits repo since-date author))) repos))) @@ -176,11 +224,14 @@ current repo if that is nil) and displays them in a `*magit-standup*' buffer." (interactive) (let* ((repo-commits (magit-standup--gather)) - (buf (get-buffer-create "*magit-standup*"))) + (link-package (or magit-standup-link-package + (magit-standup--detect-link-package))) + (buf (get-buffer-create "*magit-standup*")) + (link-prefix (magit-standup--link-prefix link-package))) (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer) - (insert (magit-standup--format-org repo-commits))) + (insert (magit-standup--format-org repo-commits link-prefix))) (goto-char (point-min)) (org-mode)) (pop-to-buffer buf))) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index 9aaf64a..b482579 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -52,6 +52,48 @@ (lambda () (encode-time 0 0 12 7 1 2026)))) (expect (magit-standup--since-date) :to-equal "2025-12-31"))))) +(describe "magit-standup--detect-link-package" + (it "returns orgit when orgit is loaded" + (cl-letf (((symbol-function 'featurep) + (lambda (f) (eq f 'orgit)))) + (expect (magit-standup--detect-link-package) :to-be 'orgit))) + + (it "returns org-git-link when org-git-link is loaded" + (cl-letf (((symbol-function 'featurep) + (lambda (f) (eq f 'org-git-link)))) + (expect (magit-standup--detect-link-package) :to-be 'org-git-link))) + + (it "returns nil when nothing is loaded" + (cl-letf (((symbol-function 'featurep) + (lambda (_) nil))) + (expect (magit-standup--detect-link-package) :to-be nil)))) + +(describe "magit-standup--link-prefix" + (it "returns orgit-rev for orgit" + (expect (magit-standup--link-prefix 'orgit) :to-equal "orgit-rev")) + + (it "returns git for org-git-link" + (expect (magit-standup--link-prefix 'org-git-link) :to-equal "git")) + + (it "returns nil for none" + (expect (magit-standup--link-prefix 'none) :to-be nil)) + + (it "returns nil for nil" + (expect (magit-standup--link-prefix nil) :to-be nil))) + +(describe "magit-standup--format-commit" + (it "formats with orgit-rev link" + (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "orgit-rev") + :to-equal "[[orgit-rev:/home/user/repo::abc123][abc123]] Fix bug <2026-01-05> Alice")) + + (it "formats with git link" + (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "git") + :to-equal "[[git:/home/user/repo::abc123][abc123]] Fix bug <2026-01-05> Alice")) + + (it "formats as plain text when link-type is omitted" + (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice") + :to-equal "abc123 Fix bug <2026-01-05> Alice"))) + (describe "magit-standup--collect-commits" (it "sets default-directory to the repo path" (let (captured-dirs) @@ -132,40 +174,52 @@ (describe "magit-standup--format-org" (it "formats branch commits with subheadings" (expect (magit-standup--format-org - '(("my-repo" . (("main" . ("abc123 Fix bug" - "def456 Add feature")))))) + '(("/home/user/my-repo" . (("main" . ("abc123\0Fix bug" + "def456\0Add feature"))))) + nil) :to-equal "* my-repo\n** ~main~\n- abc123 Fix bug\n- def456 Add feature\n")) (it "shows placeholder when all branches have no commits" (expect (magit-standup--format-org - '(("empty-repo" . (("main") ("develop"))))) + '(("/home/user/empty-repo" . (("main") ("develop")))) + nil) :to-equal "* empty-repo\n- (no commits)\n")) (it "skips branches with no commits" (expect (magit-standup--format-org - '(("my-repo" . (("main" . ("abc Fix thing")) - ("stale-branch"))))) + '(("/home/user/my-repo" . (("main" . ("abc\0Fix thing")) + ("stale-branch")))) + nil) :to-equal "* my-repo\n** ~main~\n- abc Fix thing\n")) (it "shows multiple branches under one repo" (expect (magit-standup--format-org - '(("my-repo" . (("main" . ("abc Fix thing")) - ("feature" . ("def Add thing")))))) + '(("/home/user/my-repo" . (("main" . ("abc\0Fix thing")) + ("feature" . ("def\0Add thing"))))) + nil) :to-equal (concat "* my-repo\n** ~main~\n- abc Fix thing\n" "\n** ~feature~\n- def Add thing\n"))) (it "separates multiple repos with blank lines" (expect (magit-standup--format-org - '(("repo-a" . (("main" . ("abc Fix thing")))) - ("repo-b" . (("develop" . ("def Other thing")))))) + '(("/home/user/repo-a" . (("main" . ("abc\0Fix thing")))) + ("/home/user/repo-b" . (("develop" . ("def\0Other thing"))))) + nil) :to-equal (concat "* repo-a\n** ~main~\n- abc Fix thing\n" "\n" - "* repo-b\n** ~develop~\n- def Other thing\n")))) + "* repo-b\n** ~develop~\n- def Other thing\n"))) + + (it "applies link-package to commits" + (expect (magit-standup--format-org + '(("/home/user/my-repo" . (("main" . ("abc\0Fix thing"))))) + "orgit-rev") + :to-equal + "* my-repo\n** ~main~\n- [[orgit-rev:/home/user/my-repo::abc][abc]] Fix thing\n"))) (describe "magit-standup--gather" (before-each @@ -201,13 +255,13 @@ (let ((magit-standup-repos '("/tmp/a" "/tmp/b")) (magit-standup-author "alice")) (let ((result (magit-standup--gather))) - (expect (mapcar #'car result) :to-equal '("a" "b"))))) + (expect (mapcar #'car result) :to-equal '("/tmp/a" "/tmp/b"))))) (it "falls back to magit-toplevel when repos is nil" (let ((magit-standup-repos nil) (magit-standup-author "alice")) (spy-on 'magit-toplevel :and-return-value "/home/user/my-project") (let ((result (magit-standup--gather))) - (expect (mapcar #'car result) :to-equal '("my-project")))))) + (expect (mapcar #'car result) :to-equal '("/home/user/my-project")))))) ;;; magit-standup-test.el ends here From 10b5484afdbe5f732a3929c06aaf478ac1a8417e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 23:47:42 +0100 Subject: [PATCH 17/36] Remove unnecessary function --- magit-standup.el | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 075bd95..c45e430 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -102,16 +102,12 @@ today is Monday use last Friday; else use yesterday." (time-subtract now (days-to-time days-ago))))) -(defun magit-standup--git-repo-p (dir) - "Return non-nil if DIR contains a `.git' directory or file." - (file-directory-p (expand-file-name ".git" dir))) - (defun magit-standup--find-repos (dir depth) "Recursively find git repositories under DIR up to DEPTH levels. When DEPTH is nil, search with unlimited depth. Hidden directories are skipped." (cond - ((magit-standup--git-repo-p dir) (list dir)) + ((magit-git-repo-p dir) (list dir)) ((and depth (<= depth 0)) nil) (t (mapcan (lambda (child) (when (and (file-directory-p child) From 32b958afd99353bc3482b9882ba096090bdf3ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 23:49:09 +0100 Subject: [PATCH 18/36] Update docstring --- magit-standup.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index c45e430..16bd6aa 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -90,8 +90,9 @@ back to Friday (3 days), otherwise look back 1 day." (defun magit-standup--since-date () "Return the \"since\" date string for filtering commits. -If `magit-standup-since-days-ago' is set, use it. Otherwise, if -today is Monday use last Friday; else use yesterday." +If `magit-standup-since-days-ago' is set, use it. Otherwise, +on weekends look back to Friday; on Monday look back to Friday +\(3 days); on other weekdays look back 1 day." (let* ((now (current-time)) (day (string-to-number (format-time-string "%u" now))) (days-ago (cond (magit-standup-since-days-ago magit-standup-since-days-ago) From 1b9189dbb0a339fa46ea30bbc4fcf0a8c95cc5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 23:50:44 +0100 Subject: [PATCH 19/36] Skip merges --- magit-standup.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/magit-standup.el b/magit-standup.el index 16bd6aa..6ffb6ba 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -163,7 +163,9 @@ null byte." (branches (magit-git-lines "branch" "--format=%(refname:short)"))) (mapcar (lambda (branch) (cons branch - (magit-git-lines "log" "--format=%h%x00%s <%ai> - %aN" + (magit-git-lines "log" + "--no-merges" + "--format=%h%x00%s <%ai> - %aN" (concat "--after=" since-date) (concat "--author=" author) branch))) From 036a382208fb70e121ac588d793da9dee59690a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 23:51:30 +0100 Subject: [PATCH 20/36] Remove unnecessary require --- magit-standup.el | 1 - 1 file changed, 1 deletion(-) diff --git a/magit-standup.el b/magit-standup.el index 6ffb6ba..3c4a549 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -39,7 +39,6 @@ ;;; Code: (require 'magit) -(require 'seq) (defgroup magit-standup nil "Collect recent git commits for standup notes." From 2fd01a1cd2f074c4efcfff097f583f2afaf597e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 8 Feb 2026 23:56:55 +0100 Subject: [PATCH 21/36] Refactor --format-org --- magit-standup.el | 36 ++++++++++++++++++++++-------------- test/magit-standup-test.el | 16 ++++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 3c4a549..d25b9c3 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -170,6 +170,20 @@ null byte." branch))) branches))) +(defun magit-standup--format-branch-commits (repo-path branch-commits &optional link-prefix) + "Format BRANCH-COMMITS for REPO-PATH as org text. +BRANCH-COMMITS is a cons of (BRANCH-NAME . COMMITS). +LINK-PREFIX is the org link prefix string, or nil for plain text. +Returns nil when BRANCH-COMMITS has no commits." + (when (cdr branch-commits) + (concat "** ~" (car branch-commits) "~\n" + (mapconcat + (lambda (c) + (concat "- " (magit-standup--format-commit + repo-path c link-prefix))) + (cdr branch-commits) "\n") + "\n"))) + (defun magit-standup--format-org (repo-commits &optional link-prefix) "Format REPO-COMMITS as `org-mode' text. REPO-COMMITS is an alist of (REPO-PATH . BRANCH-COMMITS) where @@ -180,21 +194,15 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." (let* ((repo-path (car entry)) (repo-name (file-name-nondirectory (directory-file-name repo-path))) - (branch-commits (cdr entry)) - (active (seq-filter #'cdr branch-commits))) - (if active + (formatted (delq nil + (mapcar + (lambda (bc) + (magit-standup--format-branch-commits + repo-path bc link-prefix)) + (cdr entry))))) + (if formatted (concat "* " repo-name "\n" - (mapconcat - (lambda (bc) - (concat "** ~" (car bc) "~\n" - (mapconcat - (lambda (c) - (concat "- " (magit-standup--format-commit - repo-path c link-prefix))) - (cdr bc) "\n") - "\n")) - active - "\n")) + (mapconcat #'identity formatted "\n")) (concat "* " repo-name "\n- (no commits)\n")))) repo-commits "\n")) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index b482579..a726a3c 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -94,6 +94,22 @@ (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice") :to-equal "abc123 Fix bug <2026-01-05> Alice"))) +(describe "magit-standup--format-branch-commits" + (it "formats a branch with commits" + (expect (magit-standup--format-branch-commits + "/home/user/repo" '("main" . ("abc\0Fix bug" "def\0Add feature"))) + :to-equal "** ~main~\n- abc Fix bug\n- def Add feature\n")) + + (it "formats with a link prefix" + (expect (magit-standup--format-branch-commits + "/home/user/repo" '("main" . ("abc\0Fix bug")) "orgit-rev") + :to-equal "** ~main~\n- [[orgit-rev:/home/user/repo::abc][abc]] Fix bug\n")) + + (it "returns nil for a branch with no commits" + (expect (magit-standup--format-branch-commits + "/home/user/repo" '("stale-branch")) + :to-be nil))) + (describe "magit-standup--collect-commits" (it "sets default-directory to the repo path" (let (captured-dirs) From 54ed2304f7641271a6b2e30d5e437e35f041873c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 00:01:02 +0100 Subject: [PATCH 22/36] Update commentary --- magit-standup.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index d25b9c3..82d763b 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -26,9 +26,10 @@ ;;; Commentary: ;; Collect recent git commits across multiple repositories and format -;; them as org-mode standup notes. On Mondays it automatically looks -;; back to Friday; otherwise it looks back to the previous day. The -;; list of repositories and the lookback behavior are configurable. +;; them as org-mode standup notes. On weekends and Mondays it +;; automatically looks back to Friday; on other weekdays it looks back +;; to the previous day. The list of repositories and the lookback +;; behavior are configurable. ;; ;; Usage: ;; M-x magit-standup From 771bc7fdbc96f82a63e1b072265d1289d0b0489d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 00:02:15 +0100 Subject: [PATCH 23/36] Remove unnecessary cond --- magit-standup.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 82d763b..854c48d 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -130,8 +130,7 @@ Returns `orgit' if orgit is loaded, `org-git-link' if org-git-link is loaded, or nil if neither is available." (cond ((featurep 'orgit) 'orgit) - ((featurep 'org-git-link) 'org-git-link) - (t nil))) + ((featurep 'org-git-link) 'org-git-link))) (defun magit-standup--link-prefix (package) "Return the org link type string for PACKAGE. From 03a2e32d6259e35873b16d34e8eea7656e9d41ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 00:05:01 +0100 Subject: [PATCH 24/36] Make new buffer read-only --- magit-standup.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/magit-standup.el b/magit-standup.el index 854c48d..d4b51af 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -239,7 +239,8 @@ current repo if that is nil) and displays them in a (erase-buffer) (insert (magit-standup--format-org repo-commits link-prefix))) (goto-char (point-min)) - (org-mode)) + (org-mode) + (read-only-mode 1)) (pop-to-buffer buf))) (provide 'magit-standup) From f184a39ada47e6ec16e20ac78389954d2054b61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 00:06:04 +0100 Subject: [PATCH 25/36] Fix outdated description --- test/magit-standup-test.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index a726a3c..4cb540b 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -90,7 +90,7 @@ (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "git") :to-equal "[[git:/home/user/repo::abc123][abc123]] Fix bug <2026-01-05> Alice")) - (it "formats as plain text when link-type is omitted" + (it "formats as plain text when link-prefix is omitted" (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice") :to-equal "abc123 Fix bug <2026-01-05> Alice"))) From 24b0a96cef04b302f9eedfa8ca069577a385587e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 00:09:25 +0100 Subject: [PATCH 26/36] Quit on q --- magit-standup.el | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index d4b51af..e87788c 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -88,6 +88,8 @@ back to Friday (3 days), otherwise look back 1 day." (integer :tag "Days ago")) :group 'magit-standup) +(defconst magit-standup--buffer-name "*magit-standup*") + (defun magit-standup--since-date () "Return the \"since\" date string for filtering commits. If `magit-standup-since-days-ago' is set, use it. Otherwise, @@ -222,6 +224,12 @@ Returns an alist of (REPO-PATH . BRANCH-COMMITS) suitable for (magit-standup--collect-commits repo since-date author))) repos))) +(defun magit-standup-quit () + "Quit the `*magit-standup*' buffer and kill it." + (interactive) + (when-let ((win (get-buffer-window magit-standup--buffer-name))) + (quit-window t win))) + ;;;###autoload (defun magit-standup () "Display recent git commits as `org-mode' standup notes. @@ -232,7 +240,7 @@ current repo if that is nil) and displays them in a (let* ((repo-commits (magit-standup--gather)) (link-package (or magit-standup-link-package (magit-standup--detect-link-package))) - (buf (get-buffer-create "*magit-standup*")) + (buf (get-buffer-create magit-standup--buffer-name)) (link-prefix (magit-standup--link-prefix link-package))) (with-current-buffer buf (let ((inhibit-read-only t)) @@ -240,7 +248,9 @@ current repo if that is nil) and displays them in a (insert (magit-standup--format-org repo-commits link-prefix))) (goto-char (point-min)) (org-mode) - (read-only-mode 1)) + (read-only-mode 1) + (when (bound-and-true-p evil-mode) + (evil-local-set-key 'normal "q" #'magit-standup-quit))) (pop-to-buffer buf))) (provide 'magit-standup) From 8f8331ff270e94fd1bcce9a00fb7d3fb24390800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 08:12:08 +0100 Subject: [PATCH 27/36] Get the author email per repository --- magit-standup.el | 15 +++++----- test/magit-standup-test.el | 57 ++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index e87788c..ce78df7 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -155,12 +155,16 @@ the org link prefix string, or nil for plain text." " " rest) (concat hash " " rest)))) -(defun magit-standup--collect-commits (repo-path since-date author) - "Collect commits from REPO-PATH since SINCE-DATE by AUTHOR. +(defun magit-standup--collect-commits (repo-path since-date) + "Collect commits from REPO-PATH since SINCE-DATE. Returns an alist of (BRANCH-NAME . COMMITS) where COMMITS is a list of raw commit strings with hash and message separated by a -null byte." +null byte. The author is determined from `magit-standup-author' +or `git config user.email' in REPO-PATH." (let* ((default-directory (file-name-as-directory repo-path)) + (author (or magit-standup-author + (magit-git-string "config" "user.email") + (user-error "Cannot determine author for %s; set `magit-standup-author' or git config user.email" repo-path))) (branches (magit-git-lines "branch" "--format=%(refname:short)"))) (mapcar (lambda (branch) (cons branch @@ -214,14 +218,11 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." Returns an alist of (REPO-PATH . BRANCH-COMMITS) suitable for `magit-standup--format-org'." (let* ((since-date (magit-standup--since-date)) - (author (or magit-standup-author - (magit-git-string "config" "user.email") - (user-error "Cannot determine author; set `magit-standup-author' or git config user.email"))) (repos (or (magit-standup--resolve-repos magit-standup-repos) (list (magit-toplevel))))) (mapcar (lambda (repo) (cons repo - (magit-standup--collect-commits repo since-date author))) + (magit-standup--collect-commits repo since-date))) repos))) (defun magit-standup-quit () diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index 4cb540b..b72cd45 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -112,15 +112,37 @@ (describe "magit-standup--collect-commits" (it "sets default-directory to the repo path" - (let (captured-dirs) + (let ((magit-standup-author "alice") + captured-dirs) (spy-on 'magit-git-lines :and-call-fake (lambda (&rest _) (push default-directory captured-dirs) nil)) - (magit-standup--collect-commits "/tmp/my-repo" "2026-01-05" "alice") + (magit-standup--collect-commits "/tmp/my-repo" "2026-01-05") (expect captured-dirs :not :to-be nil) (dolist (dir captured-dirs) - (expect dir :to-equal "/tmp/my-repo/"))))) + (expect dir :to-equal "/tmp/my-repo/")))) + + (it "uses magit-standup-author when set" + (let ((magit-standup-author "alice")) + (spy-on 'magit-git-string) + (spy-on 'magit-git-lines :and-return-value nil) + (magit-standup--collect-commits "/tmp/repo" "2026-01-05") + (expect 'magit-git-string :not :to-have-been-called))) + + (it "falls back to git config user.email" + (let ((magit-standup-author nil)) + (spy-on 'magit-git-string :and-return-value "bob@example.com") + (spy-on 'magit-git-lines :and-return-value nil) + (magit-standup--collect-commits "/tmp/repo" "2026-01-05") + (expect 'magit-git-string + :to-have-been-called-with "config" "user.email"))) + + (it "signals error when no author can be determined" + (let ((magit-standup-author nil)) + (spy-on 'magit-git-string :and-return-value nil) + (expect (magit-standup--collect-commits "/tmp/repo" "2026-01-05") + :to-throw 'user-error)))) (describe "magit-standup--resolve-repos" :var (tmpdir) @@ -244,38 +266,13 @@ '(("main" . ("abc Fix thing")))) (spy-on 'magit-standup--resolve-repos :and-call-fake #'identity)) - (it "uses magit-standup-author when set" - (let ((magit-standup-repos '("/tmp/repo"))) - (spy-on 'magit-git-string) - (let ((magit-standup-author "alice")) - (magit-standup--gather)) - (expect 'magit-standup--collect-commits - :to-have-been-called-with "/tmp/repo" "2026-01-05" "alice") - (expect 'magit-git-string :not :to-have-been-called))) - - (it "falls back to git config user.email" - (let ((magit-standup-repos '("/tmp/repo")) - (magit-standup-author nil)) - (spy-on 'magit-git-string :and-return-value "bob@example.com") - (magit-standup--gather) - (expect 'magit-standup--collect-commits - :to-have-been-called-with "/tmp/repo" "2026-01-05" "bob@example.com"))) - - (it "signals error when no author can be determined" - (let ((magit-standup-repos '("/tmp/repo")) - (magit-standup-author nil)) - (spy-on 'magit-git-string :and-return-value nil) - (expect (magit-standup--gather) :to-throw 'user-error))) - (it "uses magit-standup-repos when set" - (let ((magit-standup-repos '("/tmp/a" "/tmp/b")) - (magit-standup-author "alice")) + (let ((magit-standup-repos '("/tmp/a" "/tmp/b"))) (let ((result (magit-standup--gather))) (expect (mapcar #'car result) :to-equal '("/tmp/a" "/tmp/b"))))) (it "falls back to magit-toplevel when repos is nil" - (let ((magit-standup-repos nil) - (magit-standup-author "alice")) + (let ((magit-standup-repos nil)) (spy-on 'magit-toplevel :and-return-value "/home/user/my-project") (let ((result (magit-standup--gather))) (expect (mapcar #'car result) :to-equal '("/home/user/my-project")))))) From 4f7955def17e7b8943e1c6fe0913ff42267308aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 9 Feb 2026 08:21:32 +0100 Subject: [PATCH 28/36] Skip repositories without commits --- magit-standup.el | 12 ++++++------ test/magit-standup-test.el | 22 ++++++++++------------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index ce78df7..026063a 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -206,12 +206,12 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." (magit-standup--format-branch-commits repo-path bc link-prefix)) (cdr entry))))) - (if formatted - (concat "* " repo-name "\n" - (mapconcat #'identity formatted "\n")) - (concat "* " repo-name "\n- (no commits)\n")))) - repo-commits - "\n")) + (when formatted + (concat "* " repo-name "\n\n" + (mapconcat #'identity formatted "\n") + "\n" + )))) + repo-commits)) (defun magit-standup--gather () "Gather recent commits across all configured repositories. diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index b72cd45..d49a735 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -216,14 +216,13 @@ "def456\0Add feature"))))) nil) :to-equal - "* my-repo\n** ~main~\n- abc123 Fix bug\n- def456 Add feature\n")) + "* my-repo\n\n** ~main~\n- abc123 Fix bug\n- def456 Add feature\n\n")) - (it "shows placeholder when all branches have no commits" + (it "omits repos when all branches have no commits" (expect (magit-standup--format-org '(("/home/user/empty-repo" . (("main") ("develop")))) nil) - :to-equal - "* empty-repo\n- (no commits)\n")) + :to-equal "")) (it "skips branches with no commits" (expect (magit-standup--format-org @@ -231,7 +230,7 @@ ("stale-branch")))) nil) :to-equal - "* my-repo\n** ~main~\n- abc Fix thing\n")) + "* my-repo\n\n** ~main~\n- abc Fix thing\n\n")) (it "shows multiple branches under one repo" (expect (magit-standup--format-org @@ -239,8 +238,8 @@ ("feature" . ("def\0Add thing"))))) nil) :to-equal - (concat "* my-repo\n** ~main~\n- abc Fix thing\n" - "\n** ~feature~\n- def Add thing\n"))) + (concat "* my-repo\n\n** ~main~\n- abc Fix thing\n" + "\n** ~feature~\n- def Add thing\n\n"))) (it "separates multiple repos with blank lines" (expect (magit-standup--format-org @@ -248,16 +247,15 @@ ("/home/user/repo-b" . (("develop" . ("def\0Other thing"))))) nil) :to-equal - (concat "* repo-a\n** ~main~\n- abc Fix thing\n" - "\n" - "* repo-b\n** ~develop~\n- def Other thing\n"))) + (concat "* repo-a\n\n** ~main~\n- abc Fix thing\n\n" + "* repo-b\n\n** ~develop~\n- def Other thing\n\n"))) - (it "applies link-package to commits" + (it "applies link-prefix to commits" (expect (magit-standup--format-org '(("/home/user/my-repo" . (("main" . ("abc\0Fix thing"))))) "orgit-rev") :to-equal - "* my-repo\n** ~main~\n- [[orgit-rev:/home/user/my-repo::abc][abc]] Fix thing\n"))) + "* my-repo\n\n** ~main~\n- [[orgit-rev:/home/user/my-repo::abc][abc]] Fix thing\n\n"))) (describe "magit-standup--gather" (before-each From b0d0b7c4830d158c236923d979c0b6b5eaa3dcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 16:07:31 +0100 Subject: [PATCH 29/36] Fix formatting --- magit-standup.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 026063a..edb66c0 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -209,8 +209,7 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." (when formatted (concat "* " repo-name "\n\n" (mapconcat #'identity formatted "\n") - "\n" - )))) + "\n")))) repo-commits)) (defun magit-standup--gather () From abc4d3a5ffe0fc4b8c528d7ec9385f49fa5abc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 16:09:29 +0100 Subject: [PATCH 30/36] Refactor tests --- test/magit-standup-test.el | 53 +++++++++----------------------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index d49a735..3a0be3d 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -10,47 +10,18 @@ (require 'magit-standup) (describe "magit-standup--since-date" - (it "looks back 3 days on Monday (to Friday)" - ;; Monday 2026-01-05 12:00:00 UTC — %u = 1 - (let ((magit-standup-since-days-ago nil)) - (cl-letf (((symbol-function 'current-time) - (lambda () (encode-time 0 0 12 5 1 2026)))) - (expect (magit-standup--since-date) :to-equal "2026-01-02")))) - - (it "looks back 1 day on Tuesday" - ;; Tuesday 2026-01-06 12:00:00 UTC — %u = 2 - (let ((magit-standup-since-days-ago nil)) - (cl-letf (((symbol-function 'current-time) - (lambda () (encode-time 0 0 12 6 1 2026)))) - (expect (magit-standup--since-date) :to-equal "2026-01-05")))) - - (it "looks back 1 day on Wednesday" - ;; Wednesday 2026-01-07 12:00:00 UTC — %u = 3 - (let ((magit-standup-since-days-ago nil)) - (cl-letf (((symbol-function 'current-time) - (lambda () (encode-time 0 0 12 7 1 2026)))) - (expect (magit-standup--since-date) :to-equal "2026-01-06")))) - - (it "looks back to Friday on Saturday (1 day)" - ;; Saturday 2026-01-10 12:00:00 UTC — %u = 6 - (let ((magit-standup-since-days-ago nil)) - (cl-letf (((symbol-function 'current-time) - (lambda () (encode-time 0 0 12 10 1 2026)))) - (expect (magit-standup--since-date) :to-equal "2026-01-09")))) - - (it "looks back to Friday on Sunday (2 days)" - ;; Sunday 2026-01-11 12:00:00 UTC — %u = 7 - (let ((magit-standup-since-days-ago nil)) - (cl-letf (((symbol-function 'current-time) - (lambda () (encode-time 0 0 12 11 1 2026)))) - (expect (magit-standup--since-date) :to-equal "2026-01-09")))) - - (it "uses custom override when magit-standup-since-days-ago is set" - ;; Wednesday 2026-01-07 — would normally be 1 day back - (let ((magit-standup-since-days-ago 7)) - (cl-letf (((symbol-function 'current-time) - (lambda () (encode-time 0 0 12 7 1 2026)))) - (expect (magit-standup--since-date) :to-equal "2025-12-31"))))) + (dolist (case '((:desc "looks back 3 days on Monday (to Friday)" :date "2026-01-05" :expected "2026-01-02") + (:desc "looks back 1 day on Tuesday" :date "2026-01-06" :expected "2026-01-05") + (:desc "looks back 1 day on Wednesday" :date "2026-01-07" :expected "2026-01-06") + (:desc "looks back to Friday on Saturday (1 day)" :date "2026-01-10" :expected "2026-01-09") + (:desc "looks back to Friday on Sunday (2 days)" :date "2026-01-11" :expected "2026-01-09") + (:desc "uses custom override when set" :date "2026-01-07" :expected "2025-12-31" :override 7))) + (it (plist-get case :desc) + (let ((magit-standup-since-days-ago (plist-get case :override)) + (current-time (date-to-time (concat (plist-get case :date) " 12:00:00")))) + (cl-letf (((symbol-function 'current-time) + (lambda () current-time))) + (expect (magit-standup--since-date) :to-equal (plist-get case :expected))))))) (describe "magit-standup--detect-link-package" (it "returns orgit when orgit is loaded" From c726d616b143cdf2d51dc3b195aaab119667c727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 17:01:47 +0100 Subject: [PATCH 31/36] Change magit-standup to use transient inputs --- Easkfile | 1 + magit-standup.el | 75 +++++++++++++++++++++++++++++++------- test/magit-standup-test.el | 40 +++++++++++++++++++- 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/Easkfile b/Easkfile index f157275..0cb16ab 100644 --- a/Easkfile +++ b/Easkfile @@ -12,6 +12,7 @@ (depends-on "emacs" "28.1") (depends-on "magit" "4.5.0") +(depends-on "transient" "0.8.0") (development (depends-on "package-lint") diff --git a/magit-standup.el b/magit-standup.el index edb66c0..e45baff 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -4,7 +4,7 @@ ;; Author: István Karaszi ;; Version: 0.1.0 -;; Package-Requires: ((emacs "28.1") (magit "4.5.0")) +;; Package-Requires: ((emacs "28.1") (magit "4.5.0") (transient "0.8.0")) ;; Keywords: tools, vc ;; URL: https://github.com/function-artisans/magit-standup @@ -40,6 +40,7 @@ ;;; Code: (require 'magit) +(require 'transient) (defgroup magit-standup nil "Collect recent git commits for standup notes." @@ -212,12 +213,15 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." "\n")))) repo-commits)) -(defun magit-standup--gather () +(defun magit-standup--gather (&optional since-date repos) "Gather recent commits across all configured repositories. +SINCE-DATE, when non-nil, overrides the computed since date. +REPOS, when non-nil, overrides `magit-standup-repos'. Returns an alist of (REPO-PATH . BRANCH-COMMITS) suitable for `magit-standup--format-org'." - (let* ((since-date (magit-standup--since-date)) - (repos (or (magit-standup--resolve-repos magit-standup-repos) + (let* ((since-date (or since-date (magit-standup--since-date))) + (repos (or repos + (magit-standup--resolve-repos magit-standup-repos) (list (magit-toplevel))))) (mapcar (lambda (repo) (cons repo @@ -230,15 +234,10 @@ Returns an alist of (REPO-PATH . BRANCH-COMMITS) suitable for (when-let ((win (get-buffer-window magit-standup--buffer-name))) (quit-window t win))) -;;;###autoload -(defun magit-standup () - "Display recent git commits as `org-mode' standup notes. -Collects commits from all repos in `magit-standup-repos' (or the -current repo if that is nil) and displays them in a -`*magit-standup*' buffer." - (interactive) - (let* ((repo-commits (magit-standup--gather)) - (link-package (or magit-standup-link-package +(defun magit-standup--display (repo-commits) + "Display REPO-COMMITS in the `*magit-standup*' buffer. +REPO-COMMITS is an alist as returned by `magit-standup--gather'." + (let* ((link-package (or magit-standup-link-package (magit-standup--detect-link-package))) (buf (get-buffer-create magit-standup--buffer-name)) (link-prefix (magit-standup--link-prefix link-package))) @@ -253,6 +252,56 @@ current repo if that is nil) and displays them in a (evil-local-set-key 'normal "q" #'magit-standup-quit))) (pop-to-buffer buf))) +(defun magit-standup--default-repos () + "Return resolved repo paths as a list of normalized directory names." + (mapcar (lambda (d) (directory-file-name (expand-file-name d))) + (or (magit-standup--resolve-repos magit-standup-repos) + (when-let ((top (magit-toplevel))) + (list top))))) + +(defun magit-standup--read-repos (prompt _initial-input _history) + "Read repo directories with `completing-read-multiple'. +PROMPT is shown to the user. Returns a comma-separated string." + (string-join (completing-read-multiple prompt (magit-standup--default-repos)) ",")) + +(transient-define-infix magit-standup--since () + :description "Since date" + :class 'transient-option + :shortarg "-d" + :argument "--since=" + :reader #'transient-read-date + :init-value (lambda (obj) + (unless (oref obj value) + (oset obj value (magit-standup--since-date))))) + +(transient-define-infix magit-standup--repos () + :description "Repositories" + :class 'transient-option + :shortarg "-r" + :argument "--repos=" + :reader #'magit-standup--read-repos) + +(transient-define-suffix magit-standup--run () + "Run the standup with the selected options." + :key "s" + :description "Show standup" + (interactive) + (let* ((args (transient-args 'magit-standup)) + (since (transient-arg-value "--since=" args)) + (repos-str (transient-arg-value "--repos=" args)) + (repos (when repos-str (split-string repos-str ",")))) + (magit-standup--display + (magit-standup--gather since repos)))) + +;;;###autoload (autoload 'magit-standup "magit-standup" nil t) +(transient-define-prefix magit-standup () + "Show a menu for generating standup notes." + ["Options" + (magit-standup--since) + (magit-standup--repos)] + ["Actions" + ("s" "Show standup" magit-standup--run)]) + (provide 'magit-standup) ;;; magit-standup.el ends here diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index 3a0be3d..c0bdd65 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -244,6 +244,44 @@ (let ((magit-standup-repos nil)) (spy-on 'magit-toplevel :and-return-value "/home/user/my-project") (let ((result (magit-standup--gather))) - (expect (mapcar #'car result) :to-equal '("/home/user/my-project")))))) + (expect (mapcar #'car result) :to-equal '("/home/user/my-project"))))) + + (it "uses provided since-date instead of computing one" + (let ((magit-standup-repos '("/tmp/a"))) + (magit-standup--gather "2026-02-01") + (expect 'magit-standup--since-date :not :to-have-been-called) + (expect 'magit-standup--collect-commits + :to-have-been-called-with "/tmp/a" "2026-02-01"))) + + (it "uses provided repos instead of configured ones" + (let ((magit-standup-repos '("/tmp/should-not-use"))) + (let ((result (magit-standup--gather nil '("/tmp/x" "/tmp/y")))) + (expect 'magit-standup--resolve-repos :not :to-have-been-called) + (expect (mapcar #'car result) :to-equal '("/tmp/x" "/tmp/y"))))) + + (it "uses both overrides together" + (let ((magit-standup-repos '("/tmp/ignored"))) + (magit-standup--gather "2026-03-01" '("/tmp/override")) + (expect 'magit-standup--since-date :not :to-have-been-called) + (expect 'magit-standup--resolve-repos :not :to-have-been-called) + (expect 'magit-standup--collect-commits + :to-have-been-called-with "/tmp/override" "2026-03-01")))) + +(describe "magit-standup--display" + (after-each + (when-let ((buf (get-buffer magit-standup--buffer-name))) + (kill-buffer buf))) + + (it "creates a buffer with org-mode content" + (let ((magit-standup-link-package 'none)) + (magit-standup--display + '(("/home/user/my-repo" . (("main" . ("abc\0Fix bug")))))) + (let ((buf (get-buffer magit-standup--buffer-name))) + (expect buf :not :to-be nil) + (with-current-buffer buf + (expect major-mode :to-be 'org-mode) + (expect buffer-read-only :to-be t) + (expect (buffer-string) :to-match "my-repo") + (expect (buffer-string) :to-match "abc Fix bug")))))) ;;; magit-standup-test.el ends here From 9376569bf662f8a5c2e8cc15555ccd9e52189d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 17:14:27 +0100 Subject: [PATCH 32/36] Link the repositories --- magit-standup.el | 25 +++++++++++++------------ test/magit-standup-test.el | 12 ++++++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index e45baff..8ccb793 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -151,10 +151,11 @@ the org link prefix string, or nil for plain text." (let* ((parts (split-string line "\0")) (hash (car parts)) (rest (cadr parts))) - (if link-prefix - (concat "[[" link-prefix ":" repo-path "::" hash "][" hash "]]" - " " rest) - (concat hash " " rest)))) + (format "%s %s" + (if link-prefix + (format "[[%s:%s::%s][%s]]" link-prefix repo-path hash hash) + hash) + rest))) (defun magit-standup--collect-commits (repo-path since-date) "Collect commits from REPO-PATH since SINCE-DATE. @@ -183,13 +184,13 @@ BRANCH-COMMITS is a cons of (BRANCH-NAME . COMMITS). LINK-PREFIX is the org link prefix string, or nil for plain text. Returns nil when BRANCH-COMMITS has no commits." (when (cdr branch-commits) - (concat "** ~" (car branch-commits) "~\n" + (format "** ~%s~\n%s\n" + (car branch-commits) (mapconcat (lambda (c) - (concat "- " (magit-standup--format-commit - repo-path c link-prefix))) - (cdr branch-commits) "\n") - "\n"))) + (format "- %s" (magit-standup--format-commit + repo-path c link-prefix))) + (cdr branch-commits) "\n")))) (defun magit-standup--format-org (repo-commits &optional link-prefix) "Format REPO-COMMITS as `org-mode' text. @@ -208,9 +209,9 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." repo-path bc link-prefix)) (cdr entry))))) (when formatted - (concat "* " repo-name "\n\n" - (mapconcat #'identity formatted "\n") - "\n")))) + (format "* [[file:%s][%s]]\n\n%s\n" + repo-path repo-name + (mapconcat #'identity formatted "\n"))))) repo-commits)) (defun magit-standup--gather (&optional since-date repos) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index c0bdd65..bb04a9c 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -187,7 +187,7 @@ "def456\0Add feature"))))) nil) :to-equal - "* my-repo\n\n** ~main~\n- abc123 Fix bug\n- def456 Add feature\n\n")) + "* [[file:/home/user/my-repo][my-repo]]\n\n** ~main~\n- abc123 Fix bug\n- def456 Add feature\n\n")) (it "omits repos when all branches have no commits" (expect (magit-standup--format-org @@ -201,7 +201,7 @@ ("stale-branch")))) nil) :to-equal - "* my-repo\n\n** ~main~\n- abc Fix thing\n\n")) + "* [[file:/home/user/my-repo][my-repo]]\n\n** ~main~\n- abc Fix thing\n\n")) (it "shows multiple branches under one repo" (expect (magit-standup--format-org @@ -209,7 +209,7 @@ ("feature" . ("def\0Add thing"))))) nil) :to-equal - (concat "* my-repo\n\n** ~main~\n- abc Fix thing\n" + (concat "* [[file:/home/user/my-repo][my-repo]]\n\n** ~main~\n- abc Fix thing\n" "\n** ~feature~\n- def Add thing\n\n"))) (it "separates multiple repos with blank lines" @@ -218,15 +218,15 @@ ("/home/user/repo-b" . (("develop" . ("def\0Other thing"))))) nil) :to-equal - (concat "* repo-a\n\n** ~main~\n- abc Fix thing\n\n" - "* repo-b\n\n** ~develop~\n- def Other thing\n\n"))) + (concat "* [[file:/home/user/repo-a][repo-a]]\n\n** ~main~\n- abc Fix thing\n\n" + "* [[file:/home/user/repo-b][repo-b]]\n\n** ~develop~\n- def Other thing\n\n"))) (it "applies link-prefix to commits" (expect (magit-standup--format-org '(("/home/user/my-repo" . (("main" . ("abc\0Fix thing"))))) "orgit-rev") :to-equal - "* my-repo\n\n** ~main~\n- [[orgit-rev:/home/user/my-repo::abc][abc]] Fix thing\n\n"))) + "* [[file:/home/user/my-repo][my-repo]]\n\n** ~main~\n- [[orgit-rev:/home/user/my-repo::abc][abc]] Fix thing\n\n"))) (describe "magit-standup--gather" (before-each From 1874ca00a32d3f59e5fa235c3e8a2712d4d0117c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 17:21:30 +0100 Subject: [PATCH 33/36] Extract author resolving --- magit-standup.el | 16 +++++++++++----- test/magit-standup-test.el | 36 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/magit-standup.el b/magit-standup.el index 8ccb793..c261485 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -157,16 +157,22 @@ the org link prefix string, or nil for plain text." hash) rest))) +(defun magit-standup--resolve-author (repo-path) + "Return the author string to filter commits by in REPO-PATH. +Uses `magit-standup-author' if set, otherwise falls back to +`git config user.email' in REPO-PATH." + (let ((default-directory (file-name-as-directory repo-path))) + (or magit-standup-author + (magit-git-string "config" "user.email") + (user-error "Cannot determine author for %s; set `magit-standup-author' or git config user.email" repo-path)))) + (defun magit-standup--collect-commits (repo-path since-date) "Collect commits from REPO-PATH since SINCE-DATE. Returns an alist of (BRANCH-NAME . COMMITS) where COMMITS is a list of raw commit strings with hash and message separated by a -null byte. The author is determined from `magit-standup-author' -or `git config user.email' in REPO-PATH." +null byte." (let* ((default-directory (file-name-as-directory repo-path)) - (author (or magit-standup-author - (magit-git-string "config" "user.email") - (user-error "Cannot determine author for %s; set `magit-standup-author' or git config user.email" repo-path))) + (author (magit-standup--resolve-author repo-path)) (branches (magit-git-lines "branch" "--format=%(refname:short)"))) (mapcar (lambda (branch) (cons branch diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index bb04a9c..e5cd09b 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -81,40 +81,40 @@ "/home/user/repo" '("stale-branch")) :to-be nil))) -(describe "magit-standup--collect-commits" - (it "sets default-directory to the repo path" - (let ((magit-standup-author "alice") - captured-dirs) - (spy-on 'magit-git-lines :and-call-fake - (lambda (&rest _) - (push default-directory captured-dirs) - nil)) - (magit-standup--collect-commits "/tmp/my-repo" "2026-01-05") - (expect captured-dirs :not :to-be nil) - (dolist (dir captured-dirs) - (expect dir :to-equal "/tmp/my-repo/")))) - +(describe "magit-standup--resolve-author" (it "uses magit-standup-author when set" (let ((magit-standup-author "alice")) (spy-on 'magit-git-string) - (spy-on 'magit-git-lines :and-return-value nil) - (magit-standup--collect-commits "/tmp/repo" "2026-01-05") + (magit-standup--resolve-author "/tmp/repo") (expect 'magit-git-string :not :to-have-been-called))) (it "falls back to git config user.email" (let ((magit-standup-author nil)) (spy-on 'magit-git-string :and-return-value "bob@example.com") - (spy-on 'magit-git-lines :and-return-value nil) - (magit-standup--collect-commits "/tmp/repo" "2026-01-05") + (expect (magit-standup--resolve-author "/tmp/repo") + :to-equal "bob@example.com") (expect 'magit-git-string :to-have-been-called-with "config" "user.email"))) (it "signals error when no author can be determined" (let ((magit-standup-author nil)) (spy-on 'magit-git-string :and-return-value nil) - (expect (magit-standup--collect-commits "/tmp/repo" "2026-01-05") + (expect (magit-standup--resolve-author "/tmp/repo") :to-throw 'user-error)))) +(describe "magit-standup--collect-commits" + (it "sets default-directory to the repo path" + (let ((magit-standup-author "alice") + captured-dirs) + (spy-on 'magit-git-lines :and-call-fake + (lambda (&rest _) + (push default-directory captured-dirs) + nil)) + (magit-standup--collect-commits "/tmp/my-repo" "2026-01-05") + (expect captured-dirs :not :to-be nil) + (dolist (dir captured-dirs) + (expect dir :to-equal "/tmp/my-repo/"))))) + (describe "magit-standup--resolve-repos" :var (tmpdir) From 775d17c6c823fe342e7e7d51e519ee223feedc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 17:28:11 +0100 Subject: [PATCH 34/36] Add the missing argument for Emacs 28.1 --- magit-standup.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magit-standup.el b/magit-standup.el index c261485..598f700 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -218,7 +218,7 @@ LINK-PREFIX is the org link prefix string, or nil for plain text." (format "* [[file:%s][%s]]\n\n%s\n" repo-path repo-name (mapconcat #'identity formatted "\n"))))) - repo-commits)) + repo-commits "")) (defun magit-standup--gather (&optional since-date repos) "Gather recent commits across all configured repositories. From 41380ef9d4e541a7ecb2732ae1f5025b8b80de56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 17:32:14 +0100 Subject: [PATCH 35/36] Add a README --- README.org | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 README.org diff --git a/README.org b/README.org new file mode 100644 index 0000000..41d0a91 --- /dev/null +++ b/README.org @@ -0,0 +1,62 @@ +#+title: magit-standup +#+author: István Karaszi + +Collect recent git commits across multiple repositories and format them as org-mode standup notes. + +* Features + +- Scans one or more git repositories for your recent commits +- Weekday-aware lookback: on Monday (and weekends) looks back to Friday; otherwise looks back 1 day +- Transient menu lets you override the since-date and repository list before running +- Output is an org-mode buffer with clickable commit links (via [[https://github.com/magit/orgit][orgit]] or [[https://orgmode.org/worg/org-contrib/org-git-link.html][org-git-link]]) +- Directories that aren't git repos are searched recursively for nested repos + +* Requirements + +- Emacs 28.1+ +- [[https://github.com/magit/magit][Magit]] 4.5.0+ +- [[https://github.com/magit/transient][Transient]] 0.8.0+ + +* Installation + +** With use-package and straight.el + +#+begin_src emacs-lisp +(use-package magit-standup + :straight (:host github :repo "function-artisans/magit-standup")) +#+end_src + +** Manual + +Clone the repository and add it to your =load-path=: + +#+begin_src emacs-lisp +(add-to-list 'load-path "/path/to/magit-standup") +(require 'magit-standup) +#+end_src + +* Usage + +Run =M-x magit-standup= to open the transient menu: + +- =-d= / =--since== — override the since-date (defaults to the weekday-aware computed date) +- =-r= / =--repos== — override the repository list +- =s= — show the standup buffer + +Press =s= to generate the standup notes in a =*magit-standup*= buffer. + +* Configuration + +All options can be customized via =M-x customize-group RET magit-standup=. + +| Variable | Default | Description | +|---------------------------------+---------+--------------------------------------------------------------------| +| =magit-standup-repos= | =nil= | List of directories to scan. When nil, uses current repo. | +| =magit-standup-repos-max-depth= | =1= | Max depth to search non-repo directories for nested repos. | +| =magit-standup-author= | =nil= | Author filter. When nil, uses =git config user.email=. | +| =magit-standup-since-days-ago= | =nil= | Fixed lookback override. When nil, weekday-aware logic. | +| =magit-standup-link-package= | =nil= | Link style: =orgit=, =org-git-link=, =none=, or nil (auto-detect). | + +* License + +GPL-3.0-or-later From a2353f4d318fc25a4a11a9a8ee20c87f31c2408d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Sun, 15 Feb 2026 17:39:20 +0100 Subject: [PATCH 36/36] Add a screenshot --- README.org | 2 ++ screenshots/standup-buffer.png | Bin 0 -> 65436 bytes 2 files changed, 2 insertions(+) create mode 100644 screenshots/standup-buffer.png diff --git a/README.org b/README.org index 41d0a91..fd508d6 100644 --- a/README.org +++ b/README.org @@ -3,6 +3,8 @@ Collect recent git commits across multiple repositories and format them as org-mode standup notes. +[[file:screenshots/standup-buffer.png]] + * Features - Scans one or more git repositories for your recent commits diff --git a/screenshots/standup-buffer.png b/screenshots/standup-buffer.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9c86de28339bcbd87e03678c3a306a720ef356 GIT binary patch literal 65436 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 literal 0 HcmV?d00001