;;; agitjo.el --- Manage Forgejo PRs with AGit-Flow  -*- lexical-binding: t; -*-

;; Copyright (C) 2025-2026 Alvin Hsu
;; Copyright (C) 2026 Thanos Apollo

;; Author: Alvin Hsu <aurtzy@gmail.com>
;; Homepage: https://codeberg.org/halvin/agitjo
;; Keywords: convenience, vc, tools

;; Package-Version: 2.0.0
;; Package-Revision: v2.0.0-0-g47f20e04ed09
;; Package-Requires: ((emacs "30.1")
;;                    (magit "4.3.8")
;;                    (markdown-mode "2.7")
;;                    (transient "0.9.1"))

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

;;; Commentary:

;; AGitjo extends Magit with a new menu for AGit-Flow operations, to make them
;; more convenient for users.  The AGit workflow enables users to create and
;; edit pull requests using just the "git push" command.  This package is
;; intended specifically for use with Forgejo-based (e.g. Codeberg)
;; repositories.

;; Run the following to load AGitjo and set up key-bindings in the relevant Magit
;; keymap (in this example, we use =#=):
;;   (use-package agitjo
;;     :config (agitjo-setup "#"))

;; An average workflow might look like the following:
;; 1. Write and commit some code, and eventually decide it's ready for a pull
;;    request.
;; 2. Open Agitjo's transient menu with =M-x agitjo-push= or by inputting the =#=
;;    key inside a Magit status buffer.
;; 3. Set options like the title (=-t=) and the session identifier (=-s=).
;; 4. Invoke one of the push commands to execute a git-push operation.
;; 5. If creating a new PR (the force-push option =-f= is disabled), a dedicated
;;    buffer will open for drafting a PR description.
;; 6. Visit the created pull request's link (=V=) in a browser to inspect it and
;;    make any additional adjustments, if needed.
;; 7. Repeat steps 1-4 with the force-push option (=-f=) enabled to push changes to
;;    the existing PR.

;;; Code:

;;; Initial Code.

(require 'eieio)
(require 'magit)
(require 'markdown-mode)
(require 'pcase)
(require 'project)
(require 'transient)


;;; Classes.

;;;; `agitjo-push-pullreq-suffix'.

(defclass agitjo-push-pullreq-suffix (transient-suffix)
  ((source :initarg :source
           :type function
           :documentation "Thunk that returns local source branch or reference.")
   (target :initarg :target
           :type function
           :documentation "Thunk that returns remote target branch.")))

(cl-defmethod agitjo-pullreq-source ((obj agitjo-push-pullreq-suffix))
  "Return pull request source for OBJ from calling the source slot's function."
  (funcall (oref obj source)))

(cl-defmethod agitjo-pullreq-target ((obj agitjo-push-pullreq-suffix))
  "Return pull request target for OBJ from calling the target slot's function."
  (funcall (oref obj target)))

;;;; `agitjo--pullreq-configuration'

(defvar agitjo--push-pullreq-debug? nil)

(defclass agitjo--pullreq-configuration ()
  ((type :initarg :type
         :type (satisfies agitjo--valid-pullreq-type?)
         :documentation "Pull request type.

This should always be \"for\", as Forgejo does not currently implement
this feature.")
   (source :initarg :source
           :type (satisfies magit-ref-p)
           :documentation "Source reference to use for pull request.")
   (target :initarg :target
           :type (satisfies agitjo-get-target-branch)
           :documentation "Target remote branch for pull request.")
   (args :initarg :args
         :type (list-of string)
         :documentation "Transient arguments."))
  "Class for storing push information about an AGit pull request.

The `type', `source', and `target' slots are passed to
`agitjo-pullreq-refspec' to construct a pull request refspec; see for
documentation.")

(cl-defmethod agitjo--pullreq-refspec ((config agitjo--pullreq-configuration))
  "Construct and return a pull request refspec from CONFIG.

SOURCE must a local branch.  TARGET must be a remote branch.

If the current topic for this project is non-nil, use that value as the
session.  Otherwise, the source branch name will be used."
  ;; Everything after "refs/{for|...}/{target}/" is matched as the entire
  ;; session string.  This permits "/", so it is okay if `session' is a full
  ;; refname.
  (let* ((type (oref config type))
         (source (oref config source))
         (target-branch (agitjo--pullreq-target-name config))
         (session (or (agitjo--get-current-topic) source)))
    (format "%s:refs/%s/%s/%s" source type target-branch session)))

(cl-defmethod agitjo--push-args ((config agitjo--pullreq-configuration))
  "Return arguments for CONFIG to pass to \"git push\"."
  (let* ((args (oref config args))
         (source (oref config source))
         ;; Operate with the assumption that all transient arguments prefixed
         ;; with "--" are to be passed to git push; otherwise, ignore everything
         ;; else.
         (push-args (seq-filter (lambda (arg)
                                  (and (stringp arg) (string-prefix-p "--" arg)))
                                args)))
    ;; HACK: Prepend "WIP: " to signal to Forgejo that the PR is a draft.
    ;; Eventually, when the "draft|for-review" part of PR refspecs are
    ;; implemented in Forgejo, we should be able to use that instead of doing
    ;; this.
    (when (transient-arg-value "draft" args)
      (if-let* ((title-arg-prefix "--push-option=title=")
                (title (transient-arg-value title-arg-prefix push-args))
                (title-arg-cell (member (concat title-arg-prefix title)
                                        push-args)))
          (setcar title-arg-cell (concat title-arg-prefix "WIP: " title))
        (push (concat title-arg-prefix "WIP: " (magit-rev-format "%s" source))
              push-args)))
    push-args))

(cl-defmethod agitjo--push-pullreq ((config agitjo--pullreq-configuration)
                                    &optional synchronously?)
  "Push an AGit pull request with CONFIG configuration.

By default, run asynchronously with an unspecified return value.  If
SYNCHRONOUSLY? is non-nil, wait for \"git push\" to finish before
returning, and return the exit code."
  (let ((refspec (agitjo--pullreq-refspec config))
        (remote (agitjo--pullreq-target-remote config))
        (args (agitjo--push-args config))
        ;; Output in echo area can be quite long due to the description, so
        ;; truncate it to prevent the echo area from blowing up and also to make
        ;; the (more important) beginning of the git command more visible.
        (message-truncate-lines t))
    (cond
     (agitjo--push-pullreq-debug?
      (message "debug: (remote; refspec; args): %s; %s; %S"
               remote refspec (seq-map #'substring-no-properties
                                       (flatten-list args)))
      0)
     (synchronously?
      (magit-run-git "push" "-v" remote refspec args))
     (t
      (magit-run-git-async "push" "-v" remote refspec args)))))

(cl-defmethod agitjo--pullreq-target-name ((config agitjo--pullreq-configuration))
  "Return the pull request target's name from CONFIG."
  (pcase-exhaustive (magit-split-branch-name (oref config target))
    (`(,_remote . ,name) name)))

(cl-defmethod agitjo--pullreq-target-remote ((config agitjo--pullreq-configuration))
  "Return the pull request target's remote from CONFIG."
  (pcase-exhaustive (magit-split-branch-name (oref config target))
    (`(,remote . ,_name) remote)))

(defun agitjo--valid-pullreq-type? (value)
  "Return non-nil if VALUE is a valid pull request type."
  (member value '("for" "draft" "for-review")))

;;;; `agitjo--topic-variable-infix'

(defvar agitjo--current-topics nil
  "Alist of project root to \"current topic\" session identifier.

Access and set this variable with `agitjo--get-current-topic' and
`agitjo--set-current-topic', respectively.

The session identifier is persistent per project, per Emacs session.  If
there is no associated session identifier in this variable or it is nil,
the pull request's source branch will be used by default.

Practically, the only projects that will ever have entries in this
variable will be Git projects.")

(defclass agitjo--topic-variable-infix (transient-variable)
  ((reader :initform #'agitjo--topic-reader)
   (prompt :initform "Session/topic: ")))

(cl-defmethod transient-infix-set ((_obj agitjo--topic-variable-infix) value)
  "Set the current topic for this project to VALUE."
  (agitjo--set-current-topic value))

(cl-defmethod transient-format-value ((_obj agitjo--topic-variable-infix))
  "Return the current topic as a formatted string for display."
  (concat
   "("
   (let ((topic (agitjo--get-current-topic)))
     (or (and topic (propertize topic 'face 'transient-value))
         ;; Use magit-branch-local to give a hint to the user what "source"
         ;; refers to.
         (propertize "<use source branch/ref>" 'face 'magit-branch-local)))
   ")"))

(defun agitjo--get-current-topic ()
  "Return the current session identifier associated with the current project.

May be nil."
  (if-let* ((project (project-current))
            (root (project-root project)))
      (alist-get root agitjo--current-topics nil nil #'equal)))

(defun agitjo--set-current-topic (new-topic)
  "Set the current session identifier for the current project to NEW-TOPIC."
  (if-let* ((project (project-current))
            (root (project-root project)))
      (setf (alist-get root agitjo--current-topics nil nil #'equal) new-topic)))

(defun agitjo--topic-reader (prompt initial-input history)
  "Read and return the session identifier to use.

PROMPT, INITIAL-INPUT, and HISTORY are as defined in `read-string'."
  (if (agitjo--get-current-topic)
      ;; Mimic the behavior of `transient-option', setting the variable to nil
      ;; instead of prompting when it is already set.
      nil
    (read-string prompt initial-input history)))

;;;; `agitjo--pullreq-type-switches-infix'.

(defclass agitjo--pullreq-type-switches-infix (transient-switches) ())

(cl-defmethod transient-init-value ((obj agitjo--pullreq-type-switches-infix))
  "Initialize OBJ value."
  (or (cl-call-next-method) (oset obj value "normal")))

(cl-defmethod transient-infix-read ((obj agitjo--pullreq-type-switches-infix))
  "Cycle through each switch for OBJ, excluding the no-switch option."
  (let ((choices (mapcar (apply-partially #'format (oref obj argument-format))
                         (oref obj choices)))
        (value (oref obj value)))
    (or (cadr (member value choices)) (car choices))))


;;; Modes.

;;;; Auxiliary.

(defvar agitjo-post--draft-file-name "agitjo/pullreq-draft"
  "The relative file name for AGitjo PR draft, from the repository's gitdir.")

(defvar-local agitjo-post--pullreq-config nil
  "Buffer-local storage for configuration to pass to `agitjo--push-pullreq'.")

;; Follow orderings and file names for templates as documented here:
;; <https://forgejo.org/docs/latest/user/issue-pull-request-templates/>

(defvar agitjo-post--pullreq-template-directories '(".forgejo" ".gitea" ".github")
  "Directories where pull request templates are expected.")

(defvar agitjo-post--pullreq-template-files '("PULL_REQUEST_TEMPLATE.md"
                                              "pull_request_template.md")
  "Supported pull request template file names.")

(defun agitjo-post--setup-buffer (config)
  "Set up buffer for editing pull request posts.

CONFIG is the pull request configuration that will be passed to
`agitjo--push-pullreq'."
  (let* ((buffer (agitjo-post--buffer)))
    (with-current-buffer buffer
      (agitjo-post-mode)
      (setq agitjo-post--pullreq-config config
            header-line-format
	    (substitute-command-keys
	     " \\[agitjo-post-confirm] to publish or \\[agitjo-post-cancel] to cancel."))
      (select-window (display-buffer buffer))
      (if (= (buffer-size) 0)
          (agitjo-post--replace-buffer-with-new-description config)
        (magit-read-char-case "A previous draft exists: " nil
          (?r "[r]esume editing this draft")
          (?d "[d]iscard and start over?"
              (agitjo-post--replace-buffer-with-new-description config)))))))

(defun agitjo-post--buffer ()
  "Find the post draft file for this repository and return its buffer."
  (let* ((gitdir (or (magit-gitdir)
                     (error "No gitdir associated with this directory")))
         (file (expand-file-name agitjo-post--draft-file-name gitdir))
         (_ (make-directory (file-name-directory file) t)))
    (find-file-noselect file)))

(defun agitjo-post--insert-git-object-contents (object)
  "Insert contents of git object OBJECT at point."
  (magit-git-insert "cat-file" "-p" object))

(defun agitjo-post--insert-source-head-commit-body (config)
  "Insert the commit message body for source head of pull request CONFIG.

If the commit message had a non-empty body, return t.  Otherwise, return
nil."
  (let* ((source (oref config source))
         (body (string-trim (with-temp-buffer
                              ;; NOTE: We can't use `magit-rev-format' because
                              ;; that only returns the first line of output.
                              (magit-rev-insert-format "%b" source)
                              (buffer-string)))))
    (if (string-empty-p body)
        nil
      (insert body)
      t)))

(defun agitjo-post--replace-buffer-with-new-description (config)
  "Replace buffer with a new initial pull request description for CONFIG."
  (let* ((template? (agitjo-post--replace-buffer-with-pullreq-template))
         (_ (goto-char (point-min)))
         (commit-body? (agitjo-post--insert-source-head-commit-body config)))
    (delete-all-space)
    ;; Separate commit and template, if both are present.
    (when (and template? commit-body?) (insert "\n\n-----\n\n"))))

(defun agitjo-post--replace-buffer-with-pullreq-template ()
  "Replace current buffer with the a pull request template, if there is any.

Return t if a template was inserted.  Otherwise, return nil."
  (erase-buffer)
  ;; TODO: Support YAML templates.
  (when-let* ((object (agitjo-post--find-pullreq-template-object)))
    (save-excursion (agitjo-post--insert-git-object-contents object))
    (when-let* ((pos-end-of-front-matter (agitjo-post--point-after-front-matter)))
      (delete-region (point) pos-end-of-front-matter))
    t))

(defun agitjo-post--point-after-front-matter ()
  "Return position of the end of the Markdown front matter at point.

If front matter could not be matched, return nil.

The \"end\" refers to the start of the next line after the second
\"---\" marker."
  (and (looking-at-p (markdown-get-yaml-metadata-start-border))
       (save-excursion
         (goto-char (pos-eol))
         (re-search-forward (markdown-get-yaml-metadata-end-border nil) nil t)
         (point))))

(defun agitjo-post--find-pullreq-template-object ()
  "Return the preferred pull request template file's object from the main branch.

May return nil if no template file is found."
  (let* ((primary-remote (magit-primary-remote))
         (branch (or (magit-main-branch) "HEAD"))
         (ref (if primary-remote (concat primary-remote "/" branch) branch))
         (files (seq-mapcat (lambda (dir)
                              (seq-map (lambda (file)
                                         (file-name-concat dir file))
                                       agitjo-post--pullreq-template-files))
                            agitjo-post--pullreq-template-directories))
         (found-files (magit-git-items "ls-tree" "-z" "-r"
                                       "--full-tree" "--name-only"
                                       ref "--" files)))
    (if (null found-files)
        nil
      (concat ref ":" (car found-files)))))

;;;; Definitions.

(defvar-keymap agitjo-post-mode-map
  "C-c C-c" #'agitjo-post-confirm
  "C-c C-k" #'agitjo-post-cancel
  "<remap> <kill-buffer>" #'agitjo-post-cancel)

(define-derived-mode agitjo-post-mode gfm-mode "AGitjo-Post"
  "Major mode for editing pull request post contents.")


;;; Commands.

;;;; Interactive functions.

;;;;; Auxiliary.

(defun agitjo-post--push-sentinel (draft-file)
  "Return a process sentinel that clears DRAFT-FILE on push success."
  (lambda (proc event)
    ;; Preserve magit messages to be shown in addition to any of our own.
    (let ((set-message-function #'set-multi-message))
      (magit-process-sentinel proc event)
      (when (memq (process-status proc) '(exit signal))
        (if (/= 0 (process-exit-status proc))
            (message "Push failed; draft preserved.")
          (when (file-exists-p draft-file)
            (with-temp-file draft-file)))))))

;;;;; Definitions.

(defun agitjo-post-cancel ()
  "Cancel pull request post."
  (interactive)
  (with-current-buffer (agitjo-post--buffer)
    (save-buffer)
    (quit-window :kill (get-buffer-window)))
  (message "Canceled post creation."))

(defun agitjo-post-confirm ()
  "Confirm pull request post."
  (interactive)
  (let ((post-buffer (agitjo-post--buffer)))
    (unless (equal (buffer-name post-buffer) (buffer-name))
      (user-error "Function called outside AGitjo post buffer"))
    (with-current-buffer post-buffer
      (let* ((option-prefix "--push-option=description=")
             (args (seq-filter (lambda (arg)
                                 (not (string-prefix-p option-prefix arg)))
                               (oref agitjo-post--pullreq-config args))))
        (oset agitjo-post--pullreq-config args
              (cons (concat option-prefix
                            (agitjo--sanitize-description
                             (buffer-string) buffer-file-coding-system))
                    args)))
      (save-buffer)
      (let ((draft-file (buffer-file-name))
            (config agitjo-post--pullreq-config))
        (quit-window :kill (get-buffer-window))
        (when-let* ((process (agitjo--push-pullreq config)))
          (set-process-sentinel process
                                (agitjo-post--push-sentinel draft-file)))))))

;;;; Transient suffixes.

;;;;; Auxiliary.

(defvar agitjo--pullreq-link-regexp "^remote:\\s-*\\(http.*/pulls/[0-9]+\\).*$"
  "Regular expression to match a pull request link response from Git output.

This regexp should have capture group 1 as the link to visit.")

(defun agitjo-get-target-branch (branch)
  "Return the target pull request branch from BRANCH.

If BRANCH is a local branch, assume that the target remote is
`magit-primary-remote' and return the associated branch for that remote
if it exists.  If there is no associated remote branch, return nil.
Otherwise, BRANCH is already a remote branch, and return it as-is."
  (if (magit-local-branch-p branch)
      (let ((remote-branch (propertize (concat (magit-primary-remote) "/" branch)
                                       'face 'magit-branch-remote)))
        (if (magit-remote-branch-p remote-branch) remote-branch nil))
    branch))

(defun agitjo--get-last-pullreq ()
  "Find and return a link to the last-interacted pull request.

May return nil if no link could be found.

This searches the Magit process buffer of the current repository."
  (with-current-buffer (magit-process-buffer t)
    (save-excursion
      (goto-char (point-max))
      ;; Heuristic matching.
      (if (re-search-backward agitjo--pullreq-link-regexp nil t)
          (substring-no-properties (match-string 1))))))

(defun agitjo--sanitize-description (string coding-system)
  "Return a \"sanitized\" version of STRING, without problematic characters.

CODING-SYSTEM denotes the character encoding that the string has been
provided in, so it can be converted to data before encoding in base64.
See `encode-coding-region' for more information.

Some characters cause errors to occur (for example, if newlines are
present, git push outputs \"fatal: push options must not have new line
characters\").  This is avoided by encoding in base64 and prefixing the
encoded result with \"{base64}\", which is detected and auto-decoded by
Forgejo."
  (concat "{base64}"
          (base64-encode-string (encode-coding-string string coding-system)
                                :no-line-break)))

;;;;; Definitions.

(transient-define-suffix agitjo-push-pullreq (args)
  "Push with AGit-Flow to create or edit a pull request.

ARGS is a list of transient arguments to be passed to \"git push\".

This implements `agitjo-push-pullreq-suffix', and acts as a template
that other suffixes can use.  The `source' and `target' slots are not
set before calling this command.  See class documentation for
information on slots."
  :class 'agitjo-push-pullreq-suffix
  (interactive (list (transient-args 'agitjo-push)))
  (let* ((obj (transient-suffix-object))
         (force-push? (transient-arg-value "--push-option=force-push=true" args))
         (source (agitjo-pullreq-source obj))
         (target (agitjo-pullreq-target obj))
         (pullreq-config (agitjo--pullreq-configuration
                          ;; TODO: Implement using pull request type from
                          ;; transient state.  Hard-code as "for" for now.
                          :type "for"
                          :source source
                          :target target
                          :args args)))
    (if force-push?
        (agitjo--push-pullreq pullreq-config)
      (agitjo-post--setup-buffer pullreq-config))))

(transient-define-suffix agitjo-push-pullreq-current-to-upstream ()
  :class 'agitjo-push-pullreq-suffix
  :source #'magit-get-current-branch
  :target (lambda ()
            (if-let* ((current-branch (magit-get-current-branch))
                      (branch (magit-get-upstream-branch))
                      (target-branch (agitjo-get-target-branch branch)))
                target-branch
              (let ((new-upstream (magit-read-remote-branch
                                   (format "Target (and set as %s upstream): "
                                           current-branch))))
                (magit-set-upstream-branch current-branch new-upstream)
                new-upstream)))
  :inapt-if-not #'magit-get-current-branch
  :description (lambda ()
                 (if-let* ((branch (magit-get-upstream-branch)))
                     (if-let* ((target-branch (agitjo-get-target-branch branch)))
                         target-branch
                       (format "@{upstream}, overwriting it (%s not in %s)"
                               branch (propertize (magit-primary-remote)
                                                  'face 'magit-branch-remote)))
                   "@{upstream}, setting it"))
  (interactive)
  (call-interactively #'agitjo-push-pullreq))

(transient-define-suffix agitjo-push-pullreq-current ()
  :class 'agitjo-push-pullreq-suffix
  :source #'magit-get-current-branch
  :target (lambda () (magit-read-remote-branch "Target: "))
  :inapt-if-not #'magit-get-current-branch
  :description "elsewhere"
  (interactive)
  (call-interactively #'agitjo-push-pullreq))

(transient-define-suffix agitjo-push-pullreq-local-branch ()
  "Push AGit-Flow PR from some local branch to some remote branch."
  :class 'agitjo-push-pullreq-suffix
  :source (lambda () (magit-read-local-branch "Source: "))
  :target (lambda () (magit-read-remote-branch "Target: "))
  :description (propertize "local branch" 'face 'magit-branch-local)
  (interactive)
  (call-interactively #'agitjo-push-pullreq))

(transient-define-suffix agitjo-push-pullreq-local-branch-or-ref ()
  :class 'agitjo-push-pullreq-suffix
  :source (lambda () (magit-read-local-branch-or-ref "Source: "))
  :target (lambda () (magit-read-remote-branch "Target: "))
  :description (propertize "local branch or ref" 'face 'magit-branch-local)
  (interactive)
  (call-interactively #'agitjo-push-pullreq))

(transient-define-suffix agitjo-visit-last-pushed-pullreq ()
  "Visit the pull request that was last pushed during this session."
  :inapt-if-not #'agitjo--get-last-pullreq
  (interactive)
  (if-let* ((link (agitjo--get-last-pullreq)))
      (browse-url link)
    (user-error "No pull request link could be found")))

;;;; Transient infixes.

;;;;; Definitions.

(transient-define-infix agitjo-force-push-switch ()
  "Force-push a pull request, if it exists (or create a new PR anyways).

If this is not passed, a new pull request will be created with the topic
identifier, even if a pull request with the same ID exists."
  :class 'transient-switch
  :argument "--push-option=force-push=true"
  :description "Force-push")

(transient-define-infix agitjo-title-option ()
  "Title of pull request.

Leave empty to use the first line of the first new Git commit."
  :class 'transient-option
  :argument "--push-option=title="
  :description "Title")

(transient-define-infix agitjo-topic-variable ()
  "Topic of the pull request.

This is an identifier string that controls which pull request is being
interacted with.  If not specified, the pull request's source branch
will be used as the topic."
  :class 'agitjo--topic-variable-infix
  :description "Session/topic")

;; This is marked as internal with the expectation that it'll be refactored when
;; Forgejo gets support for specifying pull request types in refspecs.
(transient-define-infix agitjo--pullreq-type-switches ()
  :class 'agitjo--pullreq-type-switches-infix
  :argument-format "%s"
  :argument-regexp "\\(normal\\|draft\\)"
  :choices '("normal" "draft")
  :description "PR type")

;;;; Transient prefixes.

;;;;; Auxiliary.

(defun agitjo-push--pullreq-current-description ()
  "Return description for group of commands that make PRs from current branch."
  (if-let* ((branch (magit-get-current-branch)))
      (format (propertize "Push PR from %s to" 'face 'transient-heading)
              (propertize branch 'face 'magit-branch-local))
    "Push PR from <no current branch> to"))

;;;;; Definitions.

(transient-define-prefix agitjo-push ()
  "Push to a Forgejo-based repository, using AGit-Flow."
  ["Session options"
   ("-f" agitjo-force-push-switch)
   ("-s" agitjo-topic-variable)]
  ["New-PR options"
   :pad-keys t
   ("-t" agitjo-title-option)
   ("+" agitjo--pullreq-type-switches)]
  [ :inapt-if-not magit-get-current-branch
    :description agitjo-push--pullreq-current-description
    ("u" agitjo-push-pullreq-current-to-upstream)
    ("e" agitjo-push-pullreq-current)]
  ["Push PR from"
   ("l" agitjo-push-pullreq-local-branch)
   ("r" agitjo-push-pullreq-local-branch-or-ref)]
  ["Other commands"
   :class transient-row
   ("C" "Set variables..." magit-branch-configure)
   ("V" "Visit last-pushed PR" agitjo-visit-last-pushed-pullreq)])


;;; Convenience functions.

(defun agitjo-setup (key)
  "Convenience function for setting up Agitjo.

KEY will be bound to `agitjo-push' in `magit-status-mode-map' and
`magit-dispatch'."
  (keymap-set magit-status-mode-map key #'agitjo-push)
  (transient-append-suffix 'magit-dispatch '(0 -1 -1)
    `(,key "AGit-Flow push" agitjo-push)))

;;; Provide library.
(provide 'agitjo)
;;; agitjo.el ends here
