;;; bmp.el --- Version bumper for git projects -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2025 Abhinav Tushar

;; Author: Abhinav Tushar <lepisma@fastmail.com>
;; Package-Version: 0.6.2
;; Package-Revision: 63aeacf69a12
;; Package-Requires: ((emacs "29"))
;; URL: https://git.sr.ht/~lepisma/bmp

;;; Commentary:

;; Version bumper for git projects.  Assumes semver.
;; This file is not a part of GNU Emacs.

;;; License:

;; 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 <http://www.gnu.org/licenses/>.

;;; Code:

(require 'cl-lib)
(require 'project)
(require 'vc-git)

(require 'bmp-base)
(require 'bmp-bmpfile)
(require 'bmp-node)
(require 'bmp-elisp)

(defgroup bmp nil
  "Version bumper for git projects."
  :prefix "bmp-"
  :group 'tools)

(defcustom bmp-project-fns
  '(bmp-bmpfile-get-project
    bmp-node-get-project
    bmp-elisp-get-project)
  "Functions (in order) for parsing current project."
  :type '(repeat function)
  :group 'bmp)

(defcustom bmp-release-branches (list "master" "main")
  "Branches where releases (and so bumps) happen."
  :type '(repeat string)
  :group 'bmp)

(defun bmp-current-project ()
  "Return current project if we are in one."
  (let ((default-directory (expand-file-name (project-root (project-current)))))
    (cl-block nil
      (dolist (fn bmp-project-fns)
        (let ((project (funcall fn)))
          (when project (cl-return project)))))))

(defun bmp-git-dirty-p ()
  "Check if git is dirty.
Git not being dirty is a precondition for bumping.  We might need some
changes here if this becomes a problem."
  (let ((diffs (mapcar #'string-trim (split-string (shell-command-to-string "git status --porcelain") "\n"))))
    (not (null (cl-remove-if (lambda (l) (or (string-prefix-p "?" l) (string-equal l ""))) diffs)))))

(defun bmp-git-release-branch-p ()
  "Check if we are on the release branch for current project."
  (let ((current-branch (car (vc-git-branches))))
    (member current-branch bmp-release-branches)))

;;;###autoload
(defun bmp ()
  "Bump version for current project."
  (interactive)
  (let ((project (bmp-current-project)))
    (cl-block nil
      (when (null project)
        (cl-return (message "No project detected")))

      (when (bmp-git-dirty-p)
        (cl-return (message "Git repository dirty, can't continue with version bumps")))

      (unless (bmp-git-release-branch-p)
        ;; TODO: This doesn't look like a good strategy. Maybe we should allow
        ;;       to create custom preconditions.
        (cl-return (message "Not on release branch %s" bmp-release-branches)))

      (let* ((version-str (oref project version-str))
             (bmp-types '(patch minor major))
             (completion-collection (mapcar (lambda (bmp-type)
                                              (cons (format "%s [v%s → v%s]"
                                                            (symbol-name bmp-type)
                                                            version-str
                                                            (bmp-bump-version-str version-str bmp-type))
                                                    bmp-type))
                                            bmp-types)))
        (when-let ((selection (completing-read "Bump Type: " completion-collection nil t)))
          (bmp-bump project (alist-get selection completion-collection nil nil #'string=)))))))

(provide 'bmp)

;;; bmp.el ends here
