;;; consult-gh.el --- Consulting GitHub Client -*- lexical-binding: t -*-

;; Copyright (C) 2023 Armin Darvish

;; Author: Armin Darvish
;; Maintainer: Armin Darvish
;; Created: 2023
;; Package-Version: 20260625.257
;; Package-Revision: 7042619362cd
;; Package-Requires: ((emacs "29.4") (consult "2.0") (markdown-mode "2.6") (ox-gfm "1.0") (yaml "1.2.0"))
;; Keywords: convenience, matching, tools, vc
;; Homepage: https://github.com/armindarvish/consult-gh

;; SPDX-License-Identifier: GPL-3.0-or-later

;; This file 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 file 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 file.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; This package provides an interactive interface to GitHub command-line
;; client (see URL `https://cli.github.com/').  It uses a consult-based minibuffer
;; completion for searching and selecting GitHub repositories, issues,
;; pull requests, codes, and etc.

;;; Code:

;;; Requirements
(unless  (executable-find "gh")
  (display-warning 'consult-gh (propertize "\"gh\" is not found on this system" 'face 'warning) :warning))

(require 'consult) ;; core dependency
(require 'json) ;; for parsing jsons
(require 'markdown-mode) ;; markdown-mode for viewing issues,prs, ...
(require 'ox-gfm) ;; for exporting org-mode to github flavored markdown
(require 'ansi-color) ;; for rendering ansi colors in shell output
(require 'yaml) ;; for parsing github action files
(require 'org) ;; for its awesomeness!
(require 'dired) ;; for uploading files
(require 'mm-decode) ;; for parsing urls
(require 'log-edit) ;; for caching commit messages
(require 'diff) ;; for seeing diffs in commits
(require 'crm) ;; for multi-selection in minibuffer
(require 'map) ;; for parsing nested maps

;;; Group

(defgroup consult-gh nil
  "Consult-based interface for GitHub CLI."
  :group 'convenience
  :group 'minibuffer
  :group 'consult
  :group 'magit
  :prefix "consult-gh-"
  :link '(url-link :tag "GitHub" "https://github.com/armindarvish/consult-gh"))

;;; Customization Variables

(defcustom consult-gh-args '("gh")
  "Command line arguments to call GitHub CLI.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-repo-list-args '("repo" "list")
  "Additional arguments for `consult-gh-repo-list'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-search-repos-args '("search" "repos" "--include-forks" "true")
  "Additional arguments for `consult-gh-search-repos'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-issue-list-args '("issue" "list" "--repo")
  "Additional arguments for `consult-gh-issue-list'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-search-issues-args '("search" "issues")
  "Additional arguments for `consult-gh-search-issues'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-pr-list-args '("pr" "list" "--repo")
  "Additional arguments for `consult-gh-pr-list'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-search-prs-args '("search" "prs")
  "Additional arguments for `consult-gh-search-prs'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-search-code-args '("search" "code")
  "Additional arguments for `consult-gh-search-code'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-search-commits-args '("search" "commits")
  "Additional arguments for `consult-gh-search-commits'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))


(defcustom consult-gh-release-list-args '("release" "list" "--repo")
  "Additional arguments for `consult-gh-release-list'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-workflow-list-args `("workflow" "list" "--json" "\"name,state,id,path\"" "--repo")
  "Additional arguments for `consult-gh-workflow-list'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-run-list-args `("run" "list" "--json" "\"attempt,conclusion,createdAt,databaseId,displayTitle,event,headBranch,headSha,name,number,startedAt,status,updatedAt,url,workflowDatabaseId,workflowName\"" "--repo")
  "Additional arguments for `consult-gh-run-list'.

The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
  :group 'consult-gh
  :type '(choice string (repeat (choice string sexp))))

(defcustom consult-gh-notifications-show-unread-only t
  "Whether to hide read notifications?"
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-notifications-args-func #'consult-gh-notifications-make-args
  "Additional arguments for `consult-gh-notifications'.

Common options include:

 - `consult-gh-notifications-make-args' Make args to see unread notifications
 - A custom function                    A function that takes
                                        no input argument."
  :group 'consult-gh
  :type '(choice (const :tag "Default Function" consult-gh-notifications-make-args)
                 (function :tag "Custom Function")))

(defcustom consult-gh-browse-url-func #'browse-url
  "What function to call for browsing a url?

The function should take at least one argument for url similar to
`browse-url'.

Common options include:

 - `browse-url'         Opens url in default browser
 - `eww-browse-url'     Open url in eww
 - `browse-url-firefox' Open url in firefox
 - `browse-url-chrome'  Open url in chrome"
  :group 'consult-gh
  :type '(choice (function :tag "Browse URL in default browser" browse-url)
                 (function :tag "Browse URL in EWW" eww-browse-url)
                 (function :tag "Browse URL in Firefox" browse-url-firefox)
                 (function :tag "Browse URL in Chrome" browse-url-chrome)
                 (function :tag "Custom Function")))

(defcustom consult-gh-switch-to-buffer-func #'switch-to-buffer
  "What function to call when switching buffers?

The function should take at least one argument for buffer similar to
`switch-to-buffer'.

Common options include:

 - `switch-to-buffer'              Switch to buffer in current window
 - `switch-to-buffer-other-window' Switch to buffer in other window
 - `switch-to-buffer-other-frame'  Switch to buffer in other frame
 - `switch-to-buffer-other-tab'    Switch to buffer in other tab"
  :group 'consult-gh
  :type '(choice (function :tag "(Default) Switch to buffer in current window" switch-to-buffer)
                 (function :tag "Switch to buffer in other window" switch-to-buffer-other-window)
                 (function :tag "Switch to buffer in other frame" switch-to-buffer-other-frame)
                 (function :tag "Switch to buffer in other tab" switch-to-buffer-other-tab)
                 (function :tag "Custom Function")))

(defcustom consult-gh-pop-to-buffer-func #'pop-to-buffer
  "What function to call when popping to buffers?

The function should take at least one argument for buffer similar to
`pop-to-buffer'.

Common options include:

 - `pop-to-buffer'                 Switch to buffer in current window
 - `switch-to-buffer-other-window' Switch to buffer in other window
 - `switch-to-buffer-other-frame'  Switch to buffer in other frame
 - `switch-to-buffer-other-tab'    Switch to buffer in other tab"
  :group 'consult-gh
  :type '(choice (function :tag "(Default) Pop to buffer in another" pop-to-buffer)
                 (function :tag "Switch to buffer in other window" switch-to-buffer-other-window)
                 (function :tag "Switch to buffer in other frame" switch-to-buffer-other-frame)
                 (function :tag "Switch to buffer in other tab" switch-to-buffer-other-tab)
                 (function :tag "Custom Function")))

(defcustom consult-gh-quit-window-func #'consult-gh-quit-window
  "What function to call when quitting windows?

The function should take two arguments similar to
`consult-gh-quit-window'.

Common options include:

 - `consult-gh-quit-window'  Quit or delete window
 - `quit-window'             Quit window"
  :group 'consult-gh
  :type '(choice (function :tag "(Default) Quit or delete window" consult-gh-quit-window)
                 (function :tag "Quit window" quit-window)
                 (function :tag "Custom Function")))

(defcustom consult-gh-dashboard-items-sources (list 'consult-gh--dashboard-assigned-to-user
                                                    'consult-gh--dashboard-mentions-user
                                                    'consult-gh--dashboard-involves-user
                                                    'consult-gh--dashboard-authored-by-user)
  "A list of sources for collecting items in `consult-gh-dashboard'.

Each source in this list is a plist that can be passed to `consult--multi'.
For an example see `consult-gh--dashboard-assigned-to-user'.  For more
details on defining sources, refer to `consult--multi' and `consult--read'
documentation."
  :group 'consult-gh
  :type '(repeat symbol))

(defcustom consult-gh-tempdir (expand-file-name "consult-gh" temporary-file-directory)
  "Temporary file directory for the `consult-gh' package.

This directory is used for storing temporary files when
pulling files for viewing."
  :group 'consult-gh
  :type 'directory)

(make-obsolete-variable 'consult-gh-crm-separator nil "1.0")

(defcustom consult-gh-temp-tempdir-time-format "%Y%m%d%I%H%M"
  "Time FORMAT-STRING for temporary directories.

This is passed as FORMAT-STRING to `format-time-string' for naming
temporary directories."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-temp-tempdir-cache 300
  "Time in seconds before making a new temp directory."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-maxnum 30
"Maximum number of items to show for list and search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-repo-maxnum consult-gh-maxnum
  "Maximum number of repos to show for list and search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-issue-maxnum consult-gh-maxnum
  "Maximum number of issues to show for list and search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-dashboard-maxnum consult-gh-maxnum
  "Maximum number of dashboard items to show for each search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-pr-maxnum consult-gh-maxnum
  "Maximum number of PRs to show for list and search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-code-maxnum consult-gh-maxnum
  "Maximum number of codes to show for list and search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-commit-maxnum consult-gh-maxnum
  "Maximum number of commits to show for search operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-release-maxnum consult-gh-maxnum
  "Maximum number of releases to show for list operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-workflow-maxnum consult-gh-maxnum
  "Maximum number of workflow actions to show for list operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-run-maxnum consult-gh-maxnum
  "Maximum number of workflow actions to show for list operations.

This is the value passed to “--limit” in the command line.
The default is set to `consult-gh-maxnum'."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-comments-maxnum 30
  "Maximum number of comments to show when viewing issues or prs.

If there are more than this many comments, the user is queried about
whether to filter comments or not."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-forks-maxnum 100
  "Maximum number of fork repositories to load when creating prs."
  :group 'consult-gh
  :type 'integer)

(defcustom consult-gh-issues-show-comments-in-view t
  "Whether to include comments in `consult-gh--issue-view'?

Not including comments makes viewing long issues faster.

Common options include:
 - \='t       Ask user how many comments to show
              when there are too many
 - an integer Show this many comments
 - \='all     Show all comments
 - \='nil     Do not show any comments

Note that when some comments are hidden `consult-gh-issue-view-comments'
can be used to load all comments."
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Ask user what to do when there are many comments" t)
                 (const :tag "Do not load comments" nil)
                 (symbol :tag "Load all comments" 'all)
                 (integer :tag "An integer for number of recent comments to load")))

(defcustom consult-gh-issues-state-to-show "open"
  "Which type of issues should be listed by `consult-gh-issue-list'?

This is what is passed to “--state” argument in the command line
when running `gh issue list`.

The possible options are “open”, “closed” or “all”."
  :group 'consult-gh
  :type '(choice (const :tag "Show open issues only" "open")
                 (const :tag "Show closed issues only" "closed")
                 (const :tag "Show all issues" "all")))


(defcustom consult-gh-dashboard-state-to-show "open"
  "Which type of issues/prs should be listed by `consult-gh-dashboard'?

This is what is passed to “--state” argument in the command line
when running `gh search issues`.

The possible options are “open”, “closed”, or nil."
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Show open issues only" "open")
                 (const :tag "Show closed issues only" "closed")
                 (const :tag "Show both closed and open issue" nil)))

(defcustom consult-gh-workflow-show-all t
  "Whether to show inactive workflows in `consult-gh-workflow-list'?

When non-nil “--all” argument is passed in the command line
to `gh workflow list`."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-run-show-all t
  "Whether to show runs of inactive workflows in `consult-gh-run-list'?

When non-nil “--all” argument is passed in the command line
to `gh run list`."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-prs-state-to-show "open"
  "Which type of PRs should be listed by `consult-gh-pr-list'?

This is what is passed to “--state” argument in the command line
when running `gh pr list`.

The possible options are “open”, “closed”, “merged”, or “all”."
  :group 'consult-gh
  :type '(choice (const :tag "Show open pull requests only" "open")
                 (const :tag "Show closed pull requests only" "closed")
                 (const :tag "Show merged pull requests" "merged")
                 (const :tag "Show all pull requests" "all")))

(defcustom consult-gh-prs-show-commits-in-view nil
  "Whether to include all commits in `consult-gh--pr-view'?

Not including all commits makes viewing long PRs faster.  Note that
when commits are hidden `consult-gh-pr-view-commits'
can be used to load all commits."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-prs-show-file-changes-in-view t
  "Whether to include file change diff in `consult-gh--pr-view'?

Not including file changes makes viewing long PRs faster.  Note that
when file changes are hidden `consult-gh-pr-view-file-changes'
can be used to load all comments."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-prs-show-comments-in-view t
  "Whether to include comments in `consult-gh--pr-view'?

Not including comments makes viewing long PRs faster.

Common options include:
 - \='t       Ask user how many comments to show
              when there are too many
 - an integer Show this many comments
 - \='all     Show all comments
 - \='nil     Do not show any comments

Note that when some comments are hidden `consult-gh-pr-view-comments'
can be used to load all comments."
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Ask user what to do when there are many comments" t)
                 (const :tag "Do not load comments" nil)
                 (symbol :tag "Load all comments" 'all)
                 (integer :tag "An integer for number of recent comments to load")))

(defcustom consult-gh-pr-create-show-similar-repos 'forks
  "Whether to show similar repos (a.k.a. forks) when creating a PR?

Common options include:
 - \='parent  Only show parent repo
 - \='forks   Only show forks of the repo
 - \='all     Show all relevant repos (forks and parents)
 - \='nil     Do not show forks"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Show forks of the repo" forks)
                 (const :tag "Only show parent repos" parent)
                 (const :tag "Show all relevant repos (forks and parent)" all)
                 (const :tag "Do not show similar repos" nil)))

(defcustom consult-gh-commits-show-comments-in-view t
  "Whether to include comments in `consult-gh--commit-view'?

Not including comments makes viewing long commits faster.

Common options include:
 - \='t       Ask user how many comments to show
              when there are too many
 - an integer Show this many comments
 - \='all     Show all comments
 - \='nil     Do not show any comments

Note that when some comments are hidden `consult-gh-commit-view-comments'
can be used to load all comments."
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Ask user what to do when there are many comments" t)
                 (const :tag "Do not load comments" nil)
                 (symbol :tag "Load all comments" 'all)
                 (integer :tag "An integer for number of recent comments to load")))

(defcustom consult-gh-large-file-warning-threshold large-file-warning-threshold
  "Threshold for size of file to require confirmation for preview/open/save.

Files larger than this value in size will require user confirmation
before previewing, opening or saving the file.

Default value is set by `large-file-warning-threshold'.
If nil, no confirmation is required."
  :group 'consult-gh
  :type '(choice integer (const :tag "Never request confirmation" nil)))

(defcustom consult-gh-prioritize-local-folder 'suggest
  "How to use the local repository for completion?

When non-nil, the git repository from the local folder, if any, is
used as initial-input value for commands such as
`consult-gh-issue-list' or `consult-gh-find-file'.  The entry can
still be changed by user input.


When nil, the git repository from the local folder
\(i.e. `default-directory')\ is added to the future history list
so it can quickly be accessed by `next-history-element' \(bound to
'\\[next-history-element]'\) when running commands such as
`consult-gh-issue-list' or `consult-gh-find-file'."

  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-preview-major-mode 'gfm-mode
  "Major mode for viewing topics.

Choices are:
  - \='nil            Use `fundamental-mode' or major-mode associated
                      with file type
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "Guess major mode from file" nil)
                 (const :tag "GitHub Flavored Markdown" gfm-mode)
                 (const :tag "Markdown Mode" markdown-mode)
                 (const :tag "Org Mode" org-mode)))


(defcustom consult-gh-repo-preview-major-mode nil
  "Major mode to preview repository READMEs.

Choices are:
- nil:              Use major mode associated with original file extension
- \='gfm-mode:      Use `gfm-mode'
- \='markdown-mode: Use `markdown-mode'
- \='org-mode:      Use `org-mode'

When nil, the major mode is automatically detected based on the README's
file extension."
  :group 'consult-gh
  :type '(choice (const :tag "Guess major mode from file" nil)
                 (const :tag "GitHub Flavored Markdown" gfm-mode)
                 (const :tag "Markdown Mode" markdown-mode)
                 (const :tag "Org Mode" org-mode)))

(defcustom consult-gh-issue-preview-major-mode consult-gh-preview-major-mode
  "Major mode to preview issues and pull requests.

By default inherits from `consult-gh-preview-major-mode'.
Choices are:
  - \='nil            Use `fundamental-mode'
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use GitHub flavor markdown mode" gfm-mode)
                 (const :tag "Use markdown mode" markdown-mode)
                 (const :tag "Use org mode" org-mode)
                 (const :tag "Use fundamental mode" nil)))

(defcustom consult-gh-release-preview-major-mode consult-gh-preview-major-mode
  "Major mode to preview releases.

By default inherits from `consult-gh-preview-major-mode'.

Choices are:
  - \='nil            Use `fundamental-mode'
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use GitHub flavor markdown mode" gfm-mode)
                 (const :tag "Use markdown mode" markdown-mode)
                 (const :tag "Use org mode" org-mode)
                 (const :tag "Use fundamental mode" nil)))

(defcustom consult-gh-workflow-preview-major-mode consult-gh-preview-major-mode
  "Major mode to preview workflows.

By default inherits from `consult-gh-preview-major-mode'.

Choices are:
  - \='nil            Use `fundamental-mode'
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use GitHub flavor markdown mode" gfm-mode)
                 (const :tag "Use markdown mode" markdown-mode)
                 (const :tag "Use org mode" org-mode)
                 (const :tag "Use fundamental mode" nil)))

(defcustom consult-gh-run-preview-major-mode consult-gh-preview-major-mode
  "Major mode to preview runs.

By default inherits from `consult-gh-preview-major-mode'.

Choices are:
  - \='nil            Use `fundamental-mode'
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use GitHub flavor markdown mode" gfm-mode)
                 (const :tag "Use markdown mode" markdown-mode)
                 (const :tag "Use org mode" org-mode)
                 (const :tag "Use fundamental mode" nil)))

(defcustom consult-gh-commit-preview-major-mode consult-gh-preview-major-mode
  "Major mode to preview commits.

By default inherits from `consult-gh-preview-major-mode'.

Choices are:
  - \='nil            Use `fundamental-mode'
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use GitHub flavor markdown mode" gfm-mode)
                 (const :tag "Use markdown mode" markdown-mode)
                 (const :tag "Use org mode" org-mode)
                 (const :tag "Use fundamental mode" nil)))

(defcustom consult-gh-topic-major-mode consult-gh-preview-major-mode
  "Major mode for editing comments on issues or pull requests.

By default inherits from `consult-gh-preview-major-mode'.

Choices are:
  - \='nil            Use `text-mode'
  - \='gfm-mode       Use `gfm-mode'
  - \='markdown-mode  Use `markdown-mode'
  - \='org-mode       Use `org-mode'"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use GitHub flavor markdown mode" gfm-mode)
                 (const :tag "Use markdown mode" markdown-mode)
                 (const :tag "Use org mode" org-mode)
                 (const :tag "Use text-mode" nil)))

(defcustom consult-gh-topic-use-capf t
  "Use `consult-gh--topics-edit-capf' for `completion-at-point'.

When non-nil, `consult-gh--topics-edit-capf' is used in
`consult-gh-topic-major-mode' buffer for autocompleting
issue/pr numbers or user names."
  :group 'consult-gh
  :type '(choice (const :tag "Use autocompletion" t)
                 (const :tag "Do not use autocompletion" nil)))


(make-obsolete-variable 'consult-gh-preview-buffer-mode "Use `consult-gh-repo-preview-major-mode', or `consult-gh-issue-preview-major-mode' instead." "1.1")

(defcustom consult-gh-favorite-orgs-list (list)
  "List of default GitHub orgs/users."
  :group 'consult-gh
  :type '(repeat (string :tag "GitHub Organization (i.e. Username)")))

(make-obsolete 'consult-gh-default-orgs-list 'consult-gh-favorite-orgs-list "2.0")

(defcustom consult-gh-preview-buffer-name "*consult-gh-preview*"
  "Default name for preview buffers."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-repo-icon " "
  "Icon used for repos."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-star-icon " "
  "Icon used for stars."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-user-icon " "
  "Icon used for users.

This is used as a prefix for users in `consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-user-prefix consult-gh-user-icon  "2.5")

(defcustom consult-gh-issue-icon "issue "
  "Icon used for issues.

This is used as a prefix for issues in `consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-issue-prefix consult-gh-issue-icon  "2.5")

(defcustom consult-gh-pr-icon "pr "
  "Icon used for pull requests.

This is used as a prefix for pull requests in
`consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-pullrequest-prefix consult-gh-pr-icon  "2.5")


(defcustom consult-gh-branch-icon " "
  "Icon used for branches.

This is used as a prefix for milestones in
`consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-branch-prefix consult-gh-branch-icon  "2.5")

(defcustom consult-gh-label-icon " "
  "Icon used for labels.

This is used as a prefix for labels in `consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-label-prefix consult-gh-label-icon  "2.5")


(defcustom consult-gh-project-icon " "
  "Icon used for projects.

This is used as a prefix for projects in
`consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-project-prefix consult-gh-project-icon  "2.5")


(defcustom consult-gh-milestone-icon " "
  "Icon used for milestones.

This is used as a prefix for milestones in
`consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(make-obsolete-variable 'consult-gh-completion-milestone-prefix consult-gh-milestone-icon  "2.5")

(defcustom consult-gh-tag-icon " "
  "Icon used for release tags.

This is used as a prefix for tags in
`consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-topic-icon " "
  "Icon used for repo  topics.

This is used as a prefix for topics in
`consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-dired-dir-icon " "
  "Icon used for directories in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (string :tag "A string")
                 (function :tag "A function that takes file name and returns a string")))

(defcustom consult-gh-dired-file-icon " "
  "Icon used for files in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (string :tag "A string")
                 (function :tag "A function that takes file name and returns a string")))

(defcustom consult-gh-dired-symlink-icon " "
  "Icon used for symlinks in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (string :tag "A string")
                 (function :tag "A function that takes file name and returns a string")))

(defcustom consult-gh-dired-commit-icon " "
  "Icon used for commits (a.k.a. submodules) in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (string :tag "A string")
                 (function :tag "A function that takes file name and returns a string")))


(defcustom consult-gh-dired-dir-face 'consult-gh-dired-directory
  "Face variable used for directories in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (variable :tag "A face variable")
                 (function :tag "A function that takes file name and returns a face variable")))

(defcustom consult-gh-dired-file-face 'consult-gh-dired-file
  "Face variable for files in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (variable :tag "A face variable")
                 (function :tag "A function that takes file name and returns a face variable")))

(defcustom consult-gh-dired-symlink-face 'consult-gh-dired-symlink
  "Face variable for symlinks in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (variable :tag "A face variable")
                 (function :tag "A function that takes file name and returns a face variable")))

(defcustom consult-gh-dired-commit-face 'consult-gh-dired-special
  "Face variable for commits (a.k.a. submodules) in `consult-gh-dired'."
  :group 'consult-gh
  :type '(choice (variable :tag "A face variable")
                 (function :tag "A function that takes file name and returns a face variable")))


(defcustom consult-gh-completion-max-items "2000"
  "Maximum number of items to load for autocomplete suggestions.

This is used in `consult-gh--topics-edit-capf'."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-show-preview nil
  "Should `consult-gh' show previews?

It turns previews on/off globally for all categories
\(repos, issues, prs, codes, files,...\)"
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-preview-key consult-preview-key
  "What key to use to show preview for `consult-gh'?

This key is bound in minibuffer, and is similar to `consult-preview-key'
\(the default\) but explicitly for `consult-gh'.
This is used for all categories \(issues, prs, codes, files, etc.\)"
  :group 'consult-gh
  :type '(choice (const :tag "Any key" any)
                 (list :tag "Debounced"
                       (const :debounce)
                       (float :tag "Seconds" 0.1)
                       (const any))
                 (const :tag "No preview" nil)
                 (key :tag "Key")
                 (repeat :tag "List of keys" key)))

(defcustom consult-gh-group-by t
  "What field to use to group the results in the minibuffer?

By default it is set to t, but can be any of:

  t           Use headers for marginalia info
  nil         Do not group
  :user       Group by repository owner
  :type       Group by candidate's type (e.g. issue, pr, ....)
  :url        Group by URL
  :date       Group by the last updated date
  :visibility Group by visibility (e.g. public or private)
  symbol    Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "Type of Item" :type)))

(defcustom consult-gh-group-repos-by consult-gh-group-by
  "What field to use to group results in repo search?

This is used in `consult-gh-search-repos'.
By default it is set to t, but can be any of:

  t           Use headers for marginalia info
  nil         Do not group
  :user       Group by repository owner
  :package    Group by package name
  :date       Group by the last updated date
  :visibility Group by visibility (e.g. public or private)
  symbol      Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "Date the repo was last updated" :date)
                 (const :tag "Visibility (i.e. public, private,...)" :visibility)))

(defcustom consult-gh-group-issues-by consult-gh-group-by
  "What field to use to group results in issue search?

This is used in `consult-gh-search-issues'.
By default it is set to t, but can be any of:

  t         Use headers for marginalia info
  nil       Do not group
  :repo     Group by repository full name
  :state    Group by status of issue (i.e. open or closed)
  :user     Group by repository owner
  :package  Group by package name
  :date     Group by the last updated date
  symbol    Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "State of issue (e.g. open or closed)" :state)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "Date the repo was last updated" :date)))

(defcustom consult-gh-group-prs-by consult-gh-group-by
  "What field to use to group results in pull request search?

This is used in `consult-gh-search-prs'.
By default it is set to t, but can be any of:

  t        Use headers for marginalia info
  nil      Do not group
  :repo    Group by repository full name
  :state   Group by status of issue (i.e. open or closed)
  :user    Group by repository owner
  :package Group by package name
  :date    Group by the last updated date
  symbol   Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "State of issue (e.g. open or closed)" :state)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "Date the repo was last updated" :date)))

(defcustom consult-gh-group-files-by consult-gh-group-by
  "What field to use to group results in file search?

This is used in `consult-gh-search-codes'.
By default it is set to t, but can be any of:

  t        Use headers for marginalia info
  nil      Do not group
  :repo    Group by repository full name
  :user    Group by repository owner
  :package Group by package name
  :path    Group by the file path
  symbol   Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "File path relative to repo's root" :path)))

(defcustom consult-gh-group-code-by consult-gh-group-by
  "What field to use to group results in code search?

This is used in `consult-gh-search-codes'.
By default it is set to t, but can be any of:

  t        Use headers for marginalia info
  nil      Do not group
  :repo    Group by repository full name
  :user    Group by repository owner
  :package Group by package name
  :path    Group by the file path
  symbol   Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "File path relative to repo's root" :path)))


(defcustom consult-gh-group-commit-by consult-gh-group-by
  "What field to use to group results in commit search?

This is used in `consult-gh-search-commits'.
By default it is set to t, but can be any of:

  t        Use headers for marginalia info
  nil      Do not group
  symbol   Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)))

(defcustom consult-gh-group-dashboard-by consult-gh-group-by
  "What field to use to group results in code search?

This is used in `consult-gh-dashboard'.
By default it is set to t, but can be any of:

  t       Use headers for marginalia info
  nil     Do not group
  :repo   Group by repository full name
  :reason Group by the reason (e.g. mentions)
  :date   Group by the last updated date
  :type   Group by candidate's type (e.g. issue, pr, ....)
  symbol  Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "The reason (e.g. mentions)" :reason)
                 (const :tag "Date the repo was last updated" :date)
                 (const :tag "Type of Item" :type)))

(defcustom consult-gh-group-notifications-by consult-gh-group-by
  "What field to use to group results in notifications?

This is used in `consult-gh-notifications'.
By default it is set to t, but can be any of:

  t       Use headers for marginalia info
  nil     Do not group
  :repo   Group by repository full name
  :reason Group by the reason (e.g. mentions, comment, ...)
  :date   Group by the last updated date
  :type   Group by candidate's type (e.g. issue, pr, ....)
  :state  Group by status of issue (i.e. unread or read)
  symbol  Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "The reason (e.g. mentions)" :reason)
                 (const :tag "Date the repo was last updated" :date)
                 (const :tag "State of issue (e.g. unread or read)" :state)
                 (const :tag "Type of Item" :type)))

(defcustom consult-gh-group-releases-by consult-gh-group-by
  "What field to use to group results in release list?

This is used in `consult-gh-release-list'.
By default it is set to t, but can be any of:

  t         Use headers for marginalia info
  nil       Do not group
  :repo     Group by repository full name
  :tagname  Group by release tag name
  :state    Group by type of release (i.e. first, latest)
  :user     Group by repository owner
  :package  Group by package name
  :date     Group by the release date
  symbol    Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "Release tag name" :tagname)
                 (const :tag "State of release (e.g. latest)" :state)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)
                 (const :tag "Date the repo was last updated" :date)))

(defcustom consult-gh-group-workflows-by consult-gh-group-by
  "What field to use to group results in workflows list?

This is used in `consult-gh-workflow-list'.
By default it is set to t, but can be any of:

  t         Use headers for marginalia info
  nil       Do not group
  :repo     Group by repository full name
  :state    Group by status of workflow (i.e. enabled, disabled)
  :user     Group by repository owner
  :package  Group by package name
  symbol    Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "State of workflow (e.g. enabled)" :state)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)))

(defcustom consult-gh-group-runs-by consult-gh-group-by
  "What field to use to group results in runs list?

This is used in `consult-gh-run-list'.
By default it is set to t, but can be any of:

  t         Use headers for marginalia info
  nil       Do not group
  :repo     Group by repository full name
  :state    Group by status of workflow (i.e. enabled, disabled)
  :user     Group by repository owner
  :package  Group by package name
  symbol    Group by another property of the candidate"
  :group 'consult-gh
  :type '(choice (const :tag "(Default) Use Headers of Marginalia Info" t)
                 (const :tag "Do Not Group" nil)
                 (const :tag "Repository's full name" :repo)
                 (const :tag "State of run (e.g. enabled)" :state)
                 (const :tag "Repository's owner" :user)
                 (const :tag "Repository's package name" :package)))

(defcustom consult-gh-default-clone-directory "~/"
  "Where should GitHub repos be cloned to by default?"
  :group 'consult-gh
  :type 'directory)

(defcustom consult-gh-default-save-directory "~/Downloads/"
  "Where should single files be saved by default?

Note that this is used for saving individual files
\(see `consult-gh--files-save-file-action'\),
and not cloning entire repositories."
  :group 'consult-gh
  :type 'directory)

(defcustom consult-gh-confirm-before-clone t
  "Should confirmation of path and name be requested before cloning?

When set to nil, the default directory
`consult-gh-default-clone-directory' and package name are used
without confirmation."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-confirm-name-before-fork nil
  "Should the new repository name be confirmed when forking a repository?

When set to nil \(default\), the original repo's name will be used,
otherwise request a name."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-confirm-before-delete-repo t
  "Should confirmation of repo name be requested before deleting?

When set to non-nil, the user is asked to type the name of repo for
confirmation.

IMPORTANT NOTE: To avoid deleting repos by accident, It is highly
recommended to set this to t."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-confirm-before-rename-repo t
  "Should confirmation of repo name be requested before renaming?

When set to non-nil, the user is asked to type the new name of repo for
confirmation.

IMPORTANT NOTE: To avoid renaming repos by accident, It is highly
recommended to set this to t."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-confirm-before-delete-release t
  "Should confirmation be requested before deleting releases?

When set to non-nil, the user is asked to confirm deletion of releases.

IMPORTANT NOTE: To avoid deleting releases by accident, It is highly
recommended to set this to t."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-ask-for-path-before-save t
  "Should file path be confirmed when saving files?

When set to nil, the default directory \(`consult-gh-default-save-directory'\),
and the buffer file name \(variable `buffer-file-name'\) are used,
otherwise a file path is requested."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-default-branch-to-load nil
  "Which branch of repository to load by default in `consult-gh-find-file'?

Possible values are:

  - nil: load the “HEAD” branch
  - A string:  loads the branch named in this string.

Note that when this is set to a specific branch,
it is used for any repository that is fetched and if the branch does not exist,
it will cause an error.  Therefore, using a specific branch is not recommended
as a general case but in temporary settings where one is sure the branch exists
on the repositories being fetched."

  :group 'consult-gh
  :type '(choice (const :tag "Loads the HEAD Branch, without confirmation"
                        nil)
                 (string :tag "Loads Specific Branch")))

(defcustom consult-gh-repo-action #'consult-gh--repo-view-action
  "What function to call when a repo is selected?

Common options include:

 - `consult-gh--repo-browse-url-action'   Opens url in default browser

 - `consult-gh--repo-browse-files-action' Open files in Emacs

 - `consult-gh--repo-view-action'         Open repository's README in Emacs

 - `consult-gh--repo-clone-action'        Clone the repository

 - `consult-gh--repo-fork-action'         Fork the repository

 - A custom function:                     A function that takes
                                          only 1 input argument,
                                          the repo candidate."
  :group 'consult-gh
  :type '(choice (function :tag "Browse the Repository URL in default browser" consult-gh--repo-browse-url-action)
                 (function :tag "Open the Repository's README in an Emacs buffer" consult-gh--repo-view-action)
                 (function :tag "Browse Branches and Files inside Emacs" consult-gh--repo-browse-files-action)
                 (function :tag "Clone Repository to local folder" consult-gh--repo-clone-action)
                 (function :tag "Fork Repository" consult-gh--repo-fork-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-issue-action #'consult-gh--issue-view-action
  "What function to call when an issue is selected?

Common options include:

 - `consult-gh--issue-browse-url-action' Opens the issue url in default browser

 - `consult-gh--issue-view-action'       Opens issue in Emacs

 - `consult-gh-forge--issue-view-action' Opens issue in `magit-forge'.
                                         \(requires `consult-gh-forge' library\)

 - A custom function                     A function that takes
                                         only 1 input argument,
                                         the issue candidate."
  :group 'consult-gh
  :type (if (featurep 'consult-gh-forge) '(choice (const :tag "Browse the Issue URL in default browser" consult-gh--issue-browse-url-action)
                                                  (const :tag "Open the Issue in an Emacs buffer" consult-gh--issue-view-action)
                                                  (const :tag "Open the Issue in a Magit/Forge buffer" consult-gh-forge--issue-view-action)
                                                  (function :tag "Custom Function"))
          '(choice (const :tag "Open the Issue URL in default browser" consult-gh--issue-browse-url-action)
                   (const :tag "Open the Issue in an Emacs buffer" consult-gh--issue-view-action)
                   (const :tag "Open the Issue in a Magit/Forge buffer" consult-gh-forge--issue-view-action)
                   (function :tag "Custom Function"))))

(defcustom consult-gh-pr-action #'consult-gh--pr-view-action
  "What function to call when a pull request is selected?

Common options include:

 - `consult-gh--pr-browse-url-action' opens the PR url in default browser

 - `consult-gh--pr-view-action'       opens PR in Emacs

 - `consult-gh-forge--pr-view-action' Open PR in a `magit-forge'
                                      \(requires `consult-gh-forge' library\)

 - A custom function                  A function that takes only
                                      1 input argument,
                                      the PR candidate."
  :group 'consult-gh
  :type (if (featurep 'consult-gh-forge) '(choice (const :tag "Browse the PR URL in default browser" #'consult-gh--pr-browse-url-action)
                                                  (const :tag "Open the PR in an Emacs buffer" #'consult-gh--pr-view-action)
                                                  (const :tag "Open the PR in a Magit/Forge buffer" #'consult-gh-forge--pr-view-action)
                                                  (function :tag "Custom Function"))
          '(choice (const :tag "Open the PR URL in default browser" consult-gh--pr-browse-url-action)
                   (const :tag "Open the PR in an Emacs buffer" consult-gh--pr-view-action)
                   (function :tag "Custom Function"))))

(defcustom consult-gh-code-action #'consult-gh--code-view-action
  "What function to call when a code is selected?

Common options include:

 - `consult-gh--code-browse-url-action' Opens the code in default browser

 - `consult-gh--pr-view-action'         Opens the code in Emacs

 - A custom function                    A function that takes
                                        only 1 input argument,
                                        the code candidate."
  :group 'consult-gh
  :type '(choice (const :tag "Browse the Code (target file) URL in default browser" consult-gh--code-browse-url-action)
                 (const :tag "Open code (target file) in an Emacs buffer" consult-gh--code-view-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-commit-action #'consult-gh--commits-view-action
  "What function to call when a commit is selected?

Common options include:

 - `consult-gh--commits-browse-url-action' Opens the commit url
 in default browser

 - `consult-gh--commits-view-action' Opens the commit in Emacs

 - A custom function                     A function that takes
                                         only 1 input argument,
                                         the file candidate."
  :group 'consult-gh
  :type '(choice (const :tag "Browse the Commit URL" consult-gh--commits-browse-url-action)
                 (const :tag "Open the Commit in an Emacs Buffer" consult-gh--commits-view-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-file-action #'consult-gh--files-view-action
  "What function to call when a file is selected?

Common options include:

 - `consult-gh--files-browse-url-action' Opens the file url in default browser

 - `consult-gh--files-view-action'       Opens the file in Emacs

 - A custom function                     A function that takes
                                         only 1 input argument,
                                         the file candidate."
  :group 'consult-gh
  :type '(choice (const :tag "Browse the File URL" consult-gh--files-browse-url-action)
                 (const :tag "Save the File to local folder" consult-gh--files-view-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-discussion-action #'consult-gh--discussion-browse-url-action
  "What function to call when a discussion is selected?

Common options include:

 - `consult-gh--discussion-browse-url-action' Opens the notification url
                                              in default browser
 - A custom function                          A function that takes
                                              only 1 input argument,
                                              the notification candidate."
  :group 'consult-gh
  :type '(choice (const :tag "Browse the Discussion URL" consult-gh--discussion-browse-url-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-notifications-action #'consult-gh--notifications-action
  "What function to call when a notification is selected?

Common options include:

 - `consult-gh--notifications-action'            Uses default action of
                                                 item type (e.g. issue,
                                                 pr, discussion,...)
 - `consult-gh--notifications-browse-url-action' Open relevant
                                                 notifications in external
                                                 browser
 - A custom function                             A function that takes
                                                 only 1 input argument,
                                                 the notification
                                                 candidate."
  :group 'consult-gh
  :type '(choice (const :tag "Use Default Action of Item Type (e.g. issue, pr, ...)" consult-gh--notifications-action)
                 (const :tag "Open relevant notifications in the browser)" consult-gh--notifications-browse-url-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-dashboard-action #'consult-gh--dashboard-action
  "What function to call when a dashboard item is selected?

Common options include:

 - `consult-gh--dashboard-action'            Uses default action of item type
                                             (e.g. issue or pr)
 - `consult-gh--dashboard-browse-url-action' Opens the link in an external
                                             browser
 - A custom function                         A function that takes
                                             only 1 input argument,
                                             the dashboard candidate."
  :group 'consult-gh
  :type '(choice (const :tag "Use Default Action of Item Type (e.g. issue, pr, ...)" consult-gh--dashboard-action)
                 (const :tag "Open Issue/PR in external browser" consult-gh--dashboard-browse-url-action)
                 (function :tag "Custom Function")))

(defcustom consult-gh-release-action #'consult-gh--release-view-action
  "What function to call when a release is selected?

Common options include:
 - `consult-gh--release-browse-url-action' Opens the release url in
                                           default browser

 - `consult-gh--release-view-action'       Opens issue in Emacs

 - A custom function                     A function that takes
                                         only 1 input argument,
                                         the release candidate."
  :group 'consult-gh
  :type  '(choice (const :tag "Open the release URL in default browser" consult-gh--release-browse-url-action)
                   (const :tag "Open the release in an Emacs buffer" consult-gh--release-view-action)
                   (function :tag "Custom Function")))

(defcustom consult-gh-workflow-action #'consult-gh--workflow-view-action
  "What function to call when a workflow is selected?

Common options include:
 - `consult-gh--workflow-browse-url-action' Opens the workflow url in
                                           default browser

 - `consult-gh--workflow-view-action'       Opens workflow in Emacs

 - A custom function                     A function that takes
                                         only 1 input argument,
                                         the workflow candidate."
  :group 'consult-gh
  :type  '(choice (const :tag "Open the workflow URL in default browser" consult-gh--workflow-browse-url-action)
                   (const :tag "Open the workflow in an Emacs buffer" consult-gh--workflow-view-action)
                   (function :tag "Custom Function")))

(defcustom consult-gh-run-action #'consult-gh--run-view-action
  "What function to call when a run is selected?

Common options include:
 - `consult-gh--run-browse-url-action' Opens the run url in
                                           default browser

 - `consult-gh--run-view-action'       Opens run in Emacs

 - A custom function                     A function that takes
                                         only 1 input argument,
                                         the run candidate."
  :group 'consult-gh
  :type  '(choice (const :tag "Open the run URL in default browser" consult-gh--run-browse-url-action)
                   (const :tag "Open the run in an Emacs buffer" consult-gh--run-view-action)
                   (function :tag "Custom Function")))

(defcustom consult-gh-highlight-matches t
  "Should queries or code snippets be highlighted in preview buffers?"
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-default-interactive-command #'consult-gh-search-repos
  "Which command should `consult-gh' call?"
  :group 'consult-gh
  :type '(choice (function :tag "(Default) Search Repositories"  consult-gh-search-repos)
                 (function :tag "List default repos of user" consult-gh-favorite-repos)
                 (function :tag "Open transient menu" consult-gh-transient)
                 (function :tag "Other custom interactive command")))

(defcustom consult-gh-use-search-to-find-name nil
  "Whether to use `consult-gh-search-repos' to find repo name.

If this is set to non-nil, consult-gh calls `consult-gh-search-repos'
to get the repo name before running `consult-gh-issue-list',
`consult-gh-pr-list', etc.

This is useful if you do not remember package names and want to do a
search first."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-pr-create-confirm-fill t
  "Whether to ask user to fill pull request body?

When creating a pull request, the user is asked whether to fill the
body of the pull requests from commits info, when this variable is non-nil."
  :group 'consult-gh
  :type 'boolean)


(defcustom consult-gh-files-use-dired-like-mode t
  "Whether to open directories in `consult-gh-dired-mode'?

When this is non-nil, directories are opened in `consult-gh-dired-mode'."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-files-reuse-dired-like-buffer t
  "Whether to use the same buffer when opening directories?

When this is non-nil, directories are opened in the same buffer,
otherwise a new `consult-gh-dired-mode' buffer is created."
  :group 'consult-gh
  :type 'boolean)

(defcustom consult-gh-commit-message-ignore-char "#"
  "The string used to ignore lines in commit messages.

When creating a commit message, lines starting with this string will be ignored."
  :group 'consult-gh
  :type 'string)


(defcustom consult-gh-workflow-template "# Short Description of the Workflow\n\nname: Name of Workflow\n\n# Controls when the action will run. Workflow runs when manually triggered using the UI\n# or API.\non:\n  workflow_dispatch:\n    # Inputs the workflow accepts.\n    inputs:\n      name:\n        # Friendly description to be shown in the UI instead of 'name'\n        description: 'Name'\n        # Default value if no value is explicitly provided\n        default: 'World'\n        # Input has to be provided for the workflow to run\n        required: true\n        # The data type of the input\n        type: string\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:\n  # This workflow contains a single job called \"greet\"\n  greet:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n    # Runs a single command using the runners shell\n    - name: Send greeting\n      run: echo \"Hello ${{ inputs.name }}\"\n"

"A template string for new workflows.

When creating a workflow from scratch, this string is
used as the initial template."
  :group 'consult-gh
  :type 'string)

(defcustom consult-gh-workflow-template-repo-sources (list)
  "A list of repos for fetching workflow templates.

Each source in this list is a string with full name of a repo.

workflow YAML files under “.github/workflows” path in each of these
repos can then be fetched and used as a when creating a new workflow."
  :group 'consult-gh
  :type '(repeat string))

;;; Other Variables

(defvar consult-gh-category 'consult-gh
  "Category symbol for the `consult-gh' package.")

(defvar consult-gh-repos-category 'consult-gh-repos
  "Category symbol for repos in `consult-gh' package.")

(defvar consult-gh-issues-category 'consult-gh-issues
  "Category symbol for issues in `consult-gh' package.")

(defvar consult-gh-prs-category 'consult-gh-prs
  "Category symbol for pull requests in `consult-gh' package.")

(defvar consult-gh-dashboard-category 'consult-gh-dashboard
  "Category symbol for mix of issues and prs in `consult-gh' package.")

(defvar consult-gh-codes-category 'consult-gh-codes
  "Category symbol for codes in `consult-gh' package.")

(defvar consult-gh-commits-category 'consult-gh-commits
  "Category symbol for commits in `consult-gh' package.")

(defvar consult-gh-notifications-category 'consult-gh-notifications
  "Category symbol for notifications in `consult-gh' package.")

(defvar consult-gh-releases-category 'consult-gh-releases
  "Category symbol for releases in `consult-gh' package.")

(defvar consult-gh-workflows-category 'consult-gh-workflows
  "Category symbol for workflows in `consult-gh' package.")

(defvar consult-gh-runs-category 'consult-gh-runs
  "Category symbol for runs in `consult-gh' package.")

(defvar consult-gh-orgs-category 'consult-gh-orgs
  "Category symbol for orgs in `consult-gh' package.")

(defvar consult-gh-files-category 'consult-gh-files
  "Category symbol for files in `consult-gh' package.")

(defvar consult-gh-branches-category 'consult-gh-branches
  "Category symbol for branches in `consult-gh' package.")

(defvar consult-gh-tags-category 'consult-gh-tags
  "Category symbol for tags in `consult-gh' package.")

(defvar consult-gh--preview-buffers-list (list)
  "List of currently open preview buffers.")

(defvar consult-gh--orgs-history nil
  "History variable for orgs used in `consult-gh-repo-list'.")

(defvar consult-gh--repos-history nil
  "History variable for repos.

This is used in `consult-gh-issue-list' and `consult-gh-pr-list'.")

(defvar consult-gh--notifications-history nil
  "History variable for notifications.

This is used in `consult-gh-notifications'.")

(defvar consult-gh--dashboard-history nil
  "History variable for dashboard.

This is used in `consult-gh-dashboard'.")

(defvar consult-gh--search-repos-history nil
  "History variable for searching repos in `consult-gh-search-repos'.")

(defvar consult-gh--search-issues-history nil
  "History variable for issues used in `consult-gh-search-issues'.")

(defvar consult-gh--search-prs-history nil
  "History variable for pull requests used in `consult-gh-search-prs'.")

(defvar consult-gh--search-code-history nil
  "History variable for codes used in `consult-gh-search-code'.")

(defvar consult-gh--search-commits-history nil
  "History variable for commits used in `consult-gh-search-commits'.")

(defvar consult-gh--files-history nil
  "History variable for files used in `consult-gh-find-file'.")

(defvar consult-gh--gitignore-templates-history nil
  "History variable for gitignore templates.")

(defvar consult-gh--license-key-history nil
  "History variable for license keys.")

(defvar consult-gh--repo-clone-extra-args-history nil
  "History variable for extra args in cloning.")

(defvar consult-gh--commit-comment-start-save nil
  "Variable to save `comment-start' before changing it.")

(defvar consult-gh--current-user-orgs nil
  "List of repos of current user.")

(defvar consult-gh--known-orgs-list nil
  "List of previously visited orgs.")

(defvar consult-gh--known-repos-list nil
  "List of previously visited repos.")

(defvar consult-gh--open-files-list nil
  "List of currently open files.")

(defvar consult-gh--open-files-buffers nil
  "List of buffers with open files.")

(defvar consult-gh--current-tempdir nil
  "Current temporary directory.")

(defvar consult-gh--async-process-buffer-name " *consult-gh-async*"
  "Name of buffer for async processes.")

(defvar consult-gh--async-log-buffer " *consult-gh-async-log*"
  "Name of buffer for logging async process errors.")

(defvar consult-gh--current-input nil
  "Current input of user query.")

(defvar consult-gh--auth-current-account nil
  "Current logged-in and active account.

This is a list of \='(USERNAME HOST IF-ACTIVE)")

(defvar consult-gh-default-host "github.com"
  "Default host of GitHub.")

(defvar-local consult-gh--topic nil
  "Topic in consult-gh preview buffers.")

(defvar-local consult-gh--upload-topic nil
  "Topic for uploading files.

This is used in Dired buffers for uploading files.")

(defvar consult-gh--upload-targets (list)
  "List of upload file targets that are open.")

(defvar consult-gh--override-group-by nil
  "Override grouping based on user input.

This is used to change grouping dynamically.")

(defvar consult-gh--issue-view-json-fields "assignees,author,body,closedAt,createdAt,labels,milestone,number,projectItems,state,title,updatedAt,url"
  "String of comma separated json fields to retrieve for viewing issues.")

(defvar consult-gh--pr-view-json-fields "additions,assignees,author,baseRefName,body,closedAt,commits,createdAt,deletions,files,headRefName,headRepository,headRepositoryOwner,headRefOid,labels,mergeable,milestone,number,projectItems,reviewDecision,reviewRequests,state,statusCheckRollup,title,updatedAt,url"
  "String of comma separated json fields to retrieve for viewing prs.")

(defvar consult-gh--release-view-json-fields "assets,author,body,createdAt,isDraft,isPrerelease,name,publishedAt,tagName,tarballUrl,targetCommitish,uploadUrl,url,zipballUrl"
  "String of comma separated json fields to retrieve for viewing releases.")

(defvar consult-gh--workflow-list-template (concat "{{range .}}" "{{.name}}" "\t" "{{.state}}" "\t" "{{printf \"%.0f\" .id}}" "\t" "{{.path}}" "\n\n" "{{end}}")
  "Template for retrieving workflows used in `consult-gh--workflow-list-builder'.")

(defvar consult-gh--run-list-template (concat "{{range .}}" "{{.name}}" "\t" "{{.status}}" "\t" "{{.conclusion}}" "\t" "{{printf \"%.0f\" .databaseId}}" "\t" "{{.headBranch}}" "\t" "{{.event}}" "\t" "{{.startedAt}}" "\t" "{{.updatedAt}}" "\t" "{{.workflowName}}" "\t" "{{printf \"%.0f\" .workflowDatabaseId}}" "\n\n" "{{end}}")
  "Template for retrieving runs used in `consult-gh--run-list-builder'.")


(defvar consult-gh--repo-view-mode-keybinding-alist '(("C-c C-<return>" . consult-gh-topics-open-in-browser)
                                                      ("C-c C-e" . consult-gh-repo-edit-settings)
                                                      ("C-c C-r" . consult-gh-repo-edit-readme))

  "Keymap alist for `consult-gh-repo-view-mode'.")

(defvar consult-gh--issue-view-mode-keybinding-alist '(("C-c C-c" . consult-gh-ctrl-c-ctrl-c)
                                                       ("C-c C-e" . consult-gh-issue-edit)
                                                       ("C-c C-<return>" . consult-gh-topics-open-in-browser))

  "Keymap alist for `consult-gh-issue-view-mode'.")

(defvar consult-gh--pr-view-mode-keybinding-alist '(("C-c C-c" . consult-gh-ctrl-c-ctrl-c)
                                                    ("C-c C-e" . consult-gh-pr-edit)
                                                    ("C-c C-m" . consult-gh-pr-merge)
                                                    ("C-c C-r" . consult-gh-pr-review)
                                                    ("C-c C-<return>" . consult-gh-topics-open-in-browser)
                                                    ("C-c C-d" . consult-gh-pr-view-diff))

  "Keymap alist for `consult-gh-pr-view-mode'.")


(defvar consult-gh--workflow-view-mode-keybinding-alist '(("C-c C-c" . consult-gh-ctrl-c-ctrl-c)
                                                          ("C-c C-r" . consult-gh-workflow-change-yaml-ref)
                                                          ("C-c C-e" . consult-gh-workflow-edit)
                                                          ("C-c C-<return>" . consult-gh-topics-open-in-browser))

  "Keymap alist for `consult-gh-workflow-view-mode'.")

(defvar consult-gh--run-view-mode-keybinding-alist '(("C-c C-c" . consult-gh-ctrl-c-ctrl-c)
                                                     ("C-c C-<return>" . consult-gh-topics-open-in-browser))

  "Keymap alist for `consult-gh-run-view-mode'.")

(defvar consult-gh--release-view-mode-keybinding-alist '(("C-c C-e" . consult-gh-release-edit)
                                                         ("C-c C-<return>" . consult-gh-topics-open-in-browser))

  "Keymap alist for `consult-gh-release-view-mode'.")

(defvar consult-gh--file-view-mode-keybinding-alist '(("C-c C-<return>" . consult-gh-topics-open-in-browser)
                                                      ("C-c C-e" . consult-gh-edit-file)
                                                      ("C-c C-'" . consult-gh-push-file))

  "Keymap alist for `consult-gh-file-view-mode'.")

(defvar consult-gh--misc-view-mode-keybinding-alist '(("C-c C-k" . consult-gh-topics-cancel)
                                                      ("C-c C-<return>" . consult-gh-topics-open-in-browser))

  "Keymap alist for `consult-gh-misc-view-mode'.")

(defvar consult-gh--topics-edit-mode-keybinding-alist '(("C-c C-c" . consult-gh-ctrl-c-ctrl-c)
                                                        ("C-c C-k" . consult-gh-topics-cancel)
                                                        ("C-c C-d" . consult-gh-pr-view-diff))

  "Keymap alist for `consult-gh-topics-edit-mode'.")

(defvar consult-gh--upload-files-mode-keybinding-alist '(("C-c C-c" . consult-gh-upload-files)
                                                         ("C-c C-k" . consult-gh-topics-cancel))

  "Keymap alist for `consult-gh-upload-files-mode'.")

(defvar consult-gh--commit-message-mode-keybinding-alist '(("M-p" . consult-gh-commit-prev-message)
                                                            ("M-n" . consult-gh-commit-next-message)
                                                            ("C-c M-s" . consult-gh-commit-save-message)
                                                            ("C-c C-d" . consult-gh-commit-view-diff))

  "Keymap alist for `consult-gh-commit-message-mode'.")

(defvar consult-gh--commit-view-mode-keybinding-alist '(("C-c C-c" . consult-gh-ctrl-c-ctrl-c)
                                                        ("C-c C-<return>" . consult-gh-topics-open-in-browser)
                                                        ("C-c C-e" . consult-gh-commit-browse-files))

  "Keymap alist for `consult-gh-commit-view-mode'.")

(defvar consult-gh--last-command nil
  "Last command for `consult-gh--embark-restart'.")


(defvar consult-gh-commit-message-instructions "\n\n# Please enter the commit message for your changes. Lines starting\n# with '#' will be ignored, and an empty message aborts the commit\n# ."
  "Instructions shown in the commit message buffer.

When creating a commit, these instructions are shown.
Every line should start with git comment character or
`consult-gh-commit-message-ignore-char'.")


(defvar-local consult-gh--dired-fold-cycle 'all
  "What is shown in the current `consult-gh-dired-mode' buffer.")

;;; Faces

(defface consult-gh-success
  '((t :inherit success))
  "The face used to show issues or PRS that are successfully dealt with.

\(e.g. “closed” issues or “merged” PRS)\ when listing or searching
issues and PRS with `consult-gh'.

By default inherits from `success'.")

(defface consult-gh-warning
  '((t :inherit warning))
  "The face to show currently open issues or PRS.

By default inherits from `warning'.")

(defface consult-gh-error
  '((t :inherit error))
  "The face to show closed PRS.

By default inherits from `error'.")

(defface consult-gh-highlight-match
  '((t :inherit consult-highlight-match))
  "Highlight match face in preview buffers.

By default, inherits from `consult-highlight-match'.")

(defface consult-gh-preview-match
  '((t :inherit consult-preview-match))
  "Highlight match face in preview buffers.

 By default, inherits from `consult-preview-match'.
This face is for example used to highlight the matches to the user's
search queries \(e.g. when using `consult-gh-search-repos')\ or
code snippets \(e.g. when using `consult-gh-search-code')\ in preview buffer.")

(defface consult-gh-default
  '((t :inherit default))
  "Default face in minibuffer annotations.

By default, inherits from `default'.")

(defface consult-gh-user
  '((t :inherit font-lock-constant-face))
  "User face in minibuffer annotations.

By default, inherits from `font-lock-constant-face'.")

(defface consult-gh-package
  '((t :inherit font-lock-type-face))
  "Package face in minibuffer annotations.

By default, inherits from `font-lock-type-face'.")

(defface consult-gh-repo
  '((t :inherit font-lock-type-face))
  "Repository face in minibuffer annotations.

By default, inherits from `font-lock-type-face'.")

(defface consult-gh-issue
  '((t :inherit warning))
  "Issue number face in minibuffer annotations.

By default, inherits from `warning'.")

(defface consult-gh-pr
  '((t :inherit font-lock-function-name-face))
  "Pull request number face in minibuffer annotations.

By default, inherits from `font-lock-function-name-face'.")

(defface consult-gh-branch
  '((t :inherit font-lock-string-face))
  "Branch face in minibuffer annotations.

By default, inherits from `font-lock-string-face'.")

(defface consult-gh-visibility
  '((t :inherit font-lock-warning-face))
  "Visibility face in minibuffer annotations.

By default, inherits from `font-lock-warning-face'.")

(defface consult-gh-date
  '((t :inherit font-lock-keyword-face))
  "Date face in minibuffer annotations.

By default, inherits from `font-lock-keyword-face'.")

(defface consult-gh-tags
  '((t :inherit font-lock-comment-face))
  "Tags/Comments face in minibuffer annotations.

By default, inherits from `font-lock-comment-face'.")

(defface consult-gh-description
  '((t :inherit font-lock-builtin-face))
  "Repository description face in minibuffer annotations.

By default, inherits from `font-lock-builtin-face'.")

(defface consult-gh-code
  '((t :inherit font-lock-variable-use-face))
  "Code snippets face in minibuffer annotations.

By default, inherits from `font-lock-variable-use-face'.")

(defface consult-gh-url
  '((t :inherit link))
  "URL face in minibuffer annotations.

By default, inherits from `link'.")

(defface consult-gh-sha
  '((t :inherit font-lock-doc-face))
  "URL face in minibuffer annotations.

By default, inherits from `font-lock-doc-face'.")

(defface consult-gh-dired-directory
  '((t :inherit dired-directory))
  "Directory face in `consult-gh-dired-mode'.

By default, inherits from `dired-directory'.")


(defface consult-gh-dired-file
  '((t :inherit default))
  "File face in `consult-gh-dired-mode'.

By default, inherits from `default'.")

(defface consult-gh-dired-symlink
  '((t :inherit dired-symlink))
  "Symlink face in `consult-gh-dired-mode'.

By default, inherits from `dired-symlink'.")

(defface consult-gh-dired-commit
  '((t :inherit dired-special))
  "Commit face in `consult-gh-dired-mode'.

By default, inherits from `dired-special'.")

;;; Minor modes

(defvar-keymap consult-gh-repo-view-mode-map
  :doc "Keymap for `consult-gh-repo-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-repo-view-mode
  "Minor-mode for viewing GitHub repositories."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-repo-view"
  :keymap consult-gh-repo-view-mode-map
  (read-only-mode +1))

(defvar-keymap consult-gh-file-view-mode-map
  :doc "Keymap for `consult-gh-file-view-mode'.")


(defun consult-gh-file-view-mode-on ()
  "Enable `consult-gh-file-view-mode'."
  (read-only-mode +1)
  (add-hook 'after-save-hook #'consult-gh--files-edit-save-buffer-hook 99 t))

(defun consult-gh-file-view-mode-off ()
  "Disable `consult-gh-file-view-mode'."
  (remove-hook 'after-save-hook #'consult-gh--files-edit-save-buffer-hook t))


;;;###autoload
(define-minor-mode consult-gh-file-view-mode
  "Minor-mode for viewing GitHub files."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-file-view"
  :keymap consult-gh-file-view-mode-map
  (cond (consult-gh-file-view-mode
         (consult-gh-file-view-mode-on))
        (t
         (consult-gh-file-view-mode-off))))

(defvar-keymap consult-gh-issue-view-mode-map
  :doc "Keymap for `consult-gh-issue-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-issue-view-mode
  "Minor-mode for viewing GitHub issues."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-issue-view"
  :keymap consult-gh-issue-view-mode-map
  (read-only-mode +1))

(defvar-keymap consult-gh-pr-view-mode-map
  :doc "Keymap for `consult-gh-pr-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-pr-view-mode
  "Minor-mode for viewing GitHub pull request."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-pr-view"
  :keymap consult-gh-pr-view-mode-map
  (read-only-mode +1))

;;; Minor modes

(defvar-keymap consult-gh-release-view-mode-map
  :doc "Keymap for `consult-gh-release-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-release-view-mode
  "Minor-mode for viewing GitHub repositories."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-release-view"
  :keymap consult-gh-release-view-mode-map
  (read-only-mode +1))

(defvar-keymap consult-gh-workflow-view-mode-map
  :doc "Keymap for `consult-gh-workflow-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-workflow-view-mode
  "Minor-mode for viewing GitHub workflows."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-workflow-view"
  :keymap consult-gh-workflow-view-mode-map
  (read-only-mode +1))

(defvar-keymap consult-gh-run-view-mode-map
  :doc "Keymap for `consult-gh-run-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-run-view-mode
  "Minor-mode for viewing GitHub action runs."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-run-view"
  :keymap consult-gh-run-view-mode-map
  (read-only-mode +1))

(defvar-keymap consult-gh-misc-view-mode-map
  :doc "Keymap for `consult-gh-misc-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-misc-view-mode
  "Minor-mode for viewing miscellanous consult-gh buffers."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-misc-view"
  :keymap consult-gh-misc-view-mode-map
  (read-only-mode +1))

(defun consult-gh-topics-edit-capf-mode-on ()
  "Enable `consult-gh-topics-edit-capf-mode'."
  (add-hook 'completion-at-point-functions #'consult-gh--topics-edit-capf -100 t)
  (add-to-list 'completion-at-point-functions #'consult-gh--topics-edit-capf))

(defun consult-gh-topics-edit-capf-mode-off ()
  "Disable `consult-gh-topics-edit-capf-mode'."
  (remove-hook 'completion-at-point-functions #'consult-gh--topics-edit-capf)
  (remove #'consult-gh--topics-edit-capf completion-at-point-functions))

;;;###autoload
(define-minor-mode consult-gh-topics-edit-capf-mode
  "Minor-mode for completion at point in `consult-gh-topics-edit-comment-mode'.

Helps with autocompleting usernames, issue numbers, etc."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-topics-edit-capf"
  :keymap nil
  (cond (consult-gh-topics-edit-capf-mode
         (consult-gh-topics-edit-capf-mode-on))
        (t
         (consult-gh-topics-edit-capf-mode-off))))

(defun consult-gh-topics-edit-header-line ()
  "Create `header-line-format' for `consult-gh-topics-edit-mode'."
  (let* ((topic consult-gh--topic)
         (repo (get-text-property 0 :repo topic))
         (type (get-text-property 0 :type topic))
         (number (get-text-property 0 :number topic))
         (new (get-text-property 0 :new topic))
         (isComment (get-text-property 0 :isComment topic))
         (cand (concat repo (and number (concat ":#" number)))))
    (add-text-properties 0 1 (list :repo repo :number number) cand)
    (list
     (concat (and new "New ") (if isComment
                                  (concat "comment on ")
                                (concat type " for "))
             (buttonize (concat (consult-gh--get-package repo) (and isComment type (concat ": " (upcase type))) (and number (concat "#" number)))
                        (lambda (&rest _)
                           (if new
                               (funcall consult-gh-repo-action cand)
                             (pcase type
                               ("issue"
                                (funcall consult-gh-issue-action cand))
                               ("pr"
                                (funcall consult-gh-pr-action cand)))))))
     ".  "
     (substitute-command-keys "When done, use `\\[consult-gh-ctrl-c-ctrl-c]' to submit or `\\[consult-gh-topics-cancel]' to cancel."))))

(defvar-keymap consult-gh-topics-edit-mode-map
  :doc "Keymap for `consult-gh-topics-edit-mode'.")

(defun consult-gh-topics-edit-mode-on ()
  "Enable `consult-gh-topics-edit-mode'."
  (setq-local header-line-format (consult-gh-topics-edit-header-line)))

(defun consult-gh-topics-edit-mode-off ()
  "Disable `consult-gh-topics-edit-mode'."
  (setq-local header-line-format nil))

;;;###autoload
(define-minor-mode consult-gh-topics-edit-mode
  "Minor-mode for editable consult-gh topics."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-topics-edit"
  :keymap consult-gh-topics-edit-mode-map
  (cond (consult-gh-topics-edit-mode
         (consult-gh-topics-edit-mode-on)
         (when consult-gh-topic-use-capf
           (consult-gh-topics-edit-capf-mode +1)))
        (t
         (consult-gh-topics-edit-mode-off)
         (if consult-gh-topics-edit-capf-mode
             (consult-gh-topics-edit-capf-mode -1)))))

(defvar-keymap consult-gh-commit-message-mode-map
  :doc "Keymap for `consult-gh-commit-message-mode'.")

(defun consult-gh-commit-message-mode-on ()
  "Enable `consult-gh-topics-edit-mode'."
  (let* ((char (shell-command-to-string "git config --get core.commentchar"))
        (char (and (stringp char)
                   (not (string-empty-p char))
                   char)))
    (setq-local consult-gh--commit-comment-start-save comment-start)
  (setq-local comment-start (or char consult-gh-commit-message-ignore-char))))

(defun consult-gh-commit-message-mode-off ()
  "Disable `consult-gh-topics-edit-mode'."
  (setq-local comment-start consult-gh--commit-comment-start-save))

;;;###autoload
(define-minor-mode consult-gh-commit-message-mode
  "Minor-mode for editing commit messages in consult-gh."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-commit-message"
  :keymap consult-gh-commit-message-mode-map
  (cond (consult-gh-commit-message-mode
         (consult-gh-commit-message-mode-on))
        (t
         (consult-gh-commit-message-mode-off))))

(defvar-keymap consult-gh-commit-view-mode-map
  :doc "Keymap for `consult-gh-commit-view-mode'.")

;;;###autoload
(define-minor-mode consult-gh-commit-view-mode
  "Minor-mode for viewing GitHub commits."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-commit-view"
  :keymap consult-gh-commit-view-mode-map
  (read-only-mode +1))

(defvar-keymap consult-gh-upload-files-mode-map
 :doc "Keymap for `consult-gh-upload-files-mode'.")

(defun consult-gh-upload-files-mode-on ()
  "Enable `consult-gh-topics-edit-mode'.")

(defun consult-gh-upload-files-mode-off ()
  "Disable `consult-gh-topics-edit-mode'.")

;;;###autoload
(define-minor-mode consult-gh-upload-files-mode
  "Minor-mode for uploading files in consult-gh."
  :init-value nil
  :global nil
  :group 'consult-gh
  :lighter " consult-gh-upload-files-mode"
  :keymap consult-gh-upload-files-mode-map
  (cond (consult-gh-upload-files-mode
         (consult-gh-upload-files-mode-on))
        (t
         (consult-gh-upload-files-mode-off))))

(defvar-keymap consult-gh-dired-mode-map
  :doc "Keymap for `consult-gh-dired-mode'."
  "p" #'consult-gh-dired-previous-line
  "n" #'consult-gh-dired-next-line
  "S-SPC" #'consult-gh-dired-previous-line
  "SPC" #'consult-gh-dired-next-line
  "<remap> <previous-line>" #'consult-gh-dired-previous-line
  "<remap> <next-line>" #'consult-gh-dired-next-line
  "<remap> <save-buffer>" #'consult-gh-dired-save-file
  "+" #'consult-gh-dired-create-file
  "m" #'consult-gh-dired-mark
  "* m" #'consult-gh-dired-mark
  "u" #'consult-gh-dired-unmark
  "* u" #'consult-gh-dired-unmark
  "* ?" #'consult-gh-dired-unmark-all-marks
  "* !" #'consult-gh-dired-unmark-all-marks
  "t" #'consult-gh-dired-toggle-marks
  "* t" #'consult-gh-dired-toggle-marks
  "U" #'consult-gh-dired-unmark-all-marks
  "D" #'consult-gh-dired-delete-file
  "C" #'consult-gh-dired-copy-file
  "R" #'consult-gh-dired-rename-file
  "g" #'consult-gh-dired-revert
  "^" #'consult-gh-dired-up-directory
  "<" #'consult-gh--dired-goto-prev-directory-header
  ">" #'consult-gh--dired-goto-next-directory-header
  "M-{" #'consult-gh-dired-prev-marked-file
  "M-}" #'consult-gh-dired-next-marked-file
  "* C-p" #'consult-gh-dired-prev-marked-file
  "* C-n" #'consult-gh-dired-next-marked-file
  "$" #'consult-gh-dired-hide-subdir
  "M-$" #'consult-gh-dired-hide-all
  "TAB" #'consult-gh-dired-hide-subdir
  "<backtab>" #'consult-gh-dired-fold-cycle
  "(" #'consult-gh-dired-hide-details
  "RET" #'consult-gh-dired-find-file
  "e" #'consult-gh-dired-find-file
  "f" #'consult-gh-dired-find-file
  "<mouse-1>" #'consult-gh-dired-mouse-find-file
  "C-m" #'consult-gh-dired-find-file
  "o" #'consult-gh-dired-find-file-other-window
  "<mouse-2>" #'consult-gh-dired-mouse-find-file-other-window
  "v" #'consult-gh-dired-file-view
  "<remap> <undo>" #'consult-gh-dired-undo
  "<remap> <advertised-undo>" #'consult-gh-dired-undo
  "q" #'quit-window)

(defvar consult-gh--dired-setup-for-evil-done nil
"Whether the evil key binding setup has been done.")

(defun consult-gh--dired-mode-map-setup-for-evil ()
  "Setup map in `consult-gh-dired-mode-map' for evil mode (if loaded)."
  (when (and (fboundp 'evil-define-key*)
             (not consult-gh--dired-setup-for-evil-done))

    (and (evil-define-key* '(normal) consult-gh-dired-mode-map
      (kbd "k") #'consult-gh-dired-previous-line
      (kbd "j") #'consult-gh-dired-next-line
      (kbd "S-SPC") #'consult-gh-dired-previous-line
      (kbd "SPC") #'consult-gh-dired-next-line
      [remap previous-line] #'consult-gh-dired-previous-line
      [remap next-line] #'consult-gh-dired-next-line
      (kbd "+") #'consult-gh-dired-create-file
      (kbd "m") #'consult-gh-dired-mark
      (kbd "u") #'consult-gh-dired-unmark
      (kbd "U") #'consult-gh-dired-unmark-all-marks
      (kbd "t") #'consult-gh-dired-toggle-marks
      (kbd "D") #'consult-gh-dired-delete-file
      (kbd "C") #'consult-gh-dired-copy-file
      (kbd "R") #'consult-gh-dired-rename-file
      (kbd "RET") #'consult-gh-dired-find-file
      (kbd "g f") #'consult-gh-dired-find-file
      (kbd "C-m") #'consult-gh-dired-find-file
      (kbd "^") #'consult-gh-dired-up-directory
      (kbd "<") #'consult-gh--dired-goto-prev-directory-header
      (kbd ">") #'consult-gh--dired-goto-next-directory-header
      (kbd "g k") #'consult-gh--dired-goto-prev-directory-header
      (kbd "g j") #'consult-gh--dired-goto-next-directory-header
      (kbd "[[") #'consult-gh--dired-goto-prev-directory-header
      (kbd "]]") #'consult-gh--dired-goto-next-directory-header
      (kbd "M-{") #'consult-gh-dired-prev-marked-file
      (kbd "M-}") #'consult-gh-dired-next-marked-file
      (kbd "$") #'consult-gh-dired-hide-subdir
      (kbd "M-$") #'consult-gh-dired-hide-all
      (kbd "TAB") #'consult-gh-dired-hide-subdir
      (kbd "<backtab>") #'consult-gh-dired-fold-cycle
      (kbd "(") #'consult-gh-dired-hide-details
      (kbd "S-<return>") #'consult-gh-dired-find-file-other-window
      (kbd "g 0") #'consult-gh-dired-find-file-other-window
      (kbd "o") #'consult-gh-dired-find-file-other-window
      (kbd "g o") #'consult-gh-dired-file-view
      (kbd "g r") #'consult-gh-dired-revert
      (kbd "q") #'quit-window)
         (setq consult-gh--dired-setup-for-evil-done t))))

(define-derived-mode consult-gh-dired-mode fundamental-mode "consult-gh-dired"
  "Major mode for viewing directories."
  (consult-gh--dired-mode-map-setup-for-evil)
  (use-local-map consult-gh-dired-mode-map)
  (setq-local major-mode 'consult-gh-dired-mode))

;;; Utility functions

(defun consult-gh--set-string-width (string width &optional prepend char)
  "Set the STRING width to a fixed value, WIDTH.

If the String is longer than WIDTH, it truncates
the string and adds an ellipsis, “...”.
If the string is shorter it adds whitespace to the string.
If PREPEND is non-nil, it truncates or adds whitespace from
the beginning of string, instead of the end.
if CHAR is non-nil, uses char instead of whitespace."
  (let* ((string (format "%s" string))
         (w (length string)))
    (when (< w width)
      (if prepend
          (setq string (format "%s%s" (make-string (- width w) (or char ?\s)) (substring string)))
        (setq string (format "%s%s" (substring string) (make-string (- width w) (or char ?\s))))))
    (when (> w width)
      (if prepend
          (setq string (format "%s%s" (propertize (substring string 0 (- w (- width 3))) 'display "...") (substring string (- w (- width 3)) w)))
        (setq string (format "%s%s" (substring string 0 (- width (+ w 3))) (propertize (substring string (- width (+ w 3)) w) 'display "...")))))
    string))

(defun consult-gh--justify-left (string prefix maxwidth &optional char)
  "Set the width of STRING+PREFIX justified from left.

It uses `consult-gh--set-string-width' and sets the width
of the concatenated of STRING+PREFIX \(e.g. “\(concat prefix string\)”\)
within MAXWIDTH or a fraction of MAXWIDTH.  This is used for aligning
 marginalia info in minibuffer when using `consult-gh'.

If optional argument CHAR is non-nil uses it insted of whitespace."
  (let ((s (length string))
        (w (length prefix)))
    (cond ((< (+ s w) (floor (/ maxwidth 2)))
           (consult-gh--set-string-width string (- (floor (/ maxwidth 2))  w) t char))
          ((< (+ s w) (floor (/ maxwidth 1.8)))
           (consult-gh--set-string-width string (- (floor (/ maxwidth 1.8))  w) t char))
          ((< (+ s w) (floor (/ maxwidth 1.6)))
           (consult-gh--set-string-width string (- (floor (/ maxwidth 1.6))  w) t char))
          ((< (+ s w) (floor (/ maxwidth 1.4)))
           (consult-gh--set-string-width string (- (floor (/ maxwidth 1.4)) w) t char))
          ((< (+ s w) (floor (/ maxwidth 1.2)))
           (consult-gh--set-string-width string (- (floor (/ maxwidth 1.2)) w) t char))
          ((< (+ s w) maxwidth)
           (consult-gh--set-string-width string (- maxwidth w) t char))
          (t string))))

(defun consult-gh--highlight-match (regexp str ignore-case)
  "Highlight REGEXP in STR.

If a regular expression contains capturing groups, only these are highlighted.
If no capturing groups are used highlight the whole match.  Case is ignored
if IGNORE-CASE is non-nil.
\(This is adapted from `consult--highlight-regexps'.\)"
  (let ((i 0))
    (while (and (let ((case-fold-search ignore-case))
                  (string-match regexp str i))
                (> (match-end 0) i))
      (let ((m (match-data)))
        (setq i (cadr m)
              m (or (cddr m) m))
        (while m
          (when (car m)
            (add-face-text-property (car m) (cadr m)
                                    'consult-gh-highlight-match nil str))
          (setq m (cddr m))))))
  str)

(defun consult-gh--whole-buffer-string (&optional buffer)
  "Get whole content of the BUFFER or current buffer.

it widens the buffer to get whole content not just narrowed region."
  (with-current-buffer (or (and (buffer-live-p buffer) buffer)  (current-buffer))
    (save-restriction
      (widen)
      (buffer-string))))

(defun consult-gh--markdown-to-org-footnotes (&optional buffer)
  "Convert Markdown style footnotes to \='org-mode style footnotes in BUFFER.

Uses simple regexp replacements."
  (let ((buffer (or buffer (current-buffer))))
    (with-current-buffer buffer
      (save-match-data
        (save-mark-and-excursion
          (save-restriction
            (goto-char (point-max))
            (insert "\n")
            (while (re-search-backward "^\\[\\^\\(?1:.*\\)\\]:\s" nil t)
              (replace-match "[fn:\\1]")))))
      nil)))

(defun consult-gh--markdown-to-org-emphasis (&optional buffer)
  "Convert markdown style markings to \='org-mode style emphasis in BUFFER.

Uses simple regexp replacements."
  (let ((buffer (or buffer (current-buffer))))
    (with-current-buffer buffer
      (save-match-data
        (save-mark-and-excursion
          (save-restriction
            (goto-char (point-min))
            (while (re-search-forward "#\\|\\*\\{1,2\\}\\|_\\{1,2\\}\\|~\\{1,2\\}\\|`+" nil t)
              (pcase (match-string-no-properties 0)
                ((and (guard (eq (char-before) ?`)) ticks)
                 (cond
                  ((= (length ticks) 3)
                   (backward-char 4)
                   (save-match-data
                     (if (re-search-forward "```\\(?1:.*\n\\)\\(?2:[[:ascii:][:nonascii:]]*?\\)```" nil t)
                       (replace-match (concat
                                       (apply #'propertize (concat  "#+begin_src " (match-string 1) "\n") (text-properties-at 0 (match-string 1)))
                                       (concat (match-string 2) "\n")
                                       (apply #'propertize "#+end_src\n" (text-properties-at 0 (match-string 1))))
                                      nil t)
                       (forward-char 4))))
                  ((not (looking-at "`"))
                   (backward-char 1)
                   (save-match-data
                     (if (re-search-forward "`\\(?1:[^`]+?\\)`" nil t)
                       (replace-match (apply #'propertize (concat "=" (match-string 1) "=") (text-properties-at 0 (match-string 1))) nil t)
                       (forward-char 1))))))
                ("#" (cond
                      ((looking-at "\s\\|#+\s")
                       (delete-char -1)
                       (insert (apply #'propertize "*" (text-properties-at 0 (match-string 0)))))

                      ((looking-at "\\+begin.+\\|\\+end.+")
                       (delete-char -1)
                       (insert (apply #'propertize ",#" (text-properties-at 0 (match-string 0)))))))

                ("**"
                 (when (or (= (point) 3)
                           (looking-back "\\(?:[[:word:][:punct:][:space:]\n]\\)\\*\\{2\\}"
                                         (max (- (point) 3) (point-min))))
                   (backward-char 2)
                   (save-match-data
                     (if (re-search-forward "\\*\\{2\\}\\(?1:[^[:space:]].*[^[:space:]]?\\)\\*\\{2\\}" (line-end-position) t)
                       (replace-match (apply #'propertize (concat "*" (match-string 1) "*") (text-properties-at 0 (match-string 0))))
                      (forward-char 2)))))

                ("*"
                 (cond
                  ((and (looking-at "\s")
                        (or  (= (point) 2)
                             (looking-back "^\s*\\*" (max (- (point) 4) (point-min)))))
                   (delete-char -1)
                   (insert "-"))
                  ((or (= (point) 2)
                       (looking-back "\\(?:[[:space:]]\\)\\*"
                                     (max (- (point) 2) (point-min))))
                   (backward-char 1)
                   (save-match-data
                     (if (re-search-forward "\\*\\(?1:[^[:space:]\\*].*?[^[:space:]]?\\)\\*" (line-end-position) t)
                       (replace-match (apply #'propertize (concat "/" (match-string 1) "/") (text-properties-at 0 (match-string 0))))
                       (forward-char 1))))))

                ("__"
                 (when (or (= (point) 3)
                           (looking-back "\\(?:[[:word:][:punct:][:space:]\n]\\)_\\{2\\}"
                                         (max (- (point) 3) (point-min))))
                   (backward-char 2)
                   (save-match-data
                     (if (re-search-forward "_\\{2\\}\\(?1:[^[:space:]].*?[^[:space:]]?\\)_\\{2\\}" (line-end-position) t)
                         (replace-match (apply #'propertize (concat "*" (match-string 1) "*") (text-properties-at 0 (match-string 0))))
                       (forward-char 2)))))

                ("_"
                 (when (or (= (point) 2)
                           (looking-back "\\(?:[[:space:]]\\)_"
                                         (max (- (point) 2) (point-min))))
                   (backward-char 1)
                   (save-match-data
                     (if (re-search-forward "_\\{1\\}\\(?1:[^[:space:]_].*\\)[^[:space:]_]?)_\\{1\\}" (line-end-position) t)
                         (replace-match (apply #'propertize (concat "/" (match-string 1) "/") (text-properties-at 0 (match-string 0))))
                       (forward-char 1)))))

                ("~~"
                 (when (or (= (point) 3)
                           (looking-back "\\(?:[[:word:][:punct:][:space:]\n]\\)~\\{2\\}"
                                         (max (- (point) 3) (point-min))))
                   (backward-char 2)
                   (save-match-data
                     (if (re-search-forward "~\\{2\\}\\(?1:[^[:space:]].*\\)?[^[:space:]]?~\\{2\\}" (line-end-position) t)
                         (replace-match (apply #'propertize (concat "+" (match-string 1) "+") (text-properties-at 0 (match-string 0))))
                       (forward-char 2)))))

                ("~"
                 (when (or (= (point) 2)
                           (looking-back "\\(?:[[:space:]]\\)~"
                                         (max (- (point) 2) (point-min))))
                   (backward-char 1)
                   (save-match-data
                     (if (re-search-forward "~\\{1\\}\\(?1:[^[:space:]].*\\)[^[:space:]]?~\\{1\\}" (line-end-position) t)
                         (replace-match (apply #'propertize (concat "+" (match-string 1) "+") (text-properties-at 0 (match-string 0))))
                       (forward-char 1)))))))))))))

(defun consult-gh--markdown-to-org-links (&optional buffer)
  "Convert markdown style links to \='org-mode links in BUFFER.

Uses simple regexp replacements."
  (let ((buffer (or buffer (current-buffer))))
    (with-current-buffer buffer
      (save-match-data
      (save-mark-and-excursion
        (save-restriction
          (goto-char (point-min))
          (while (re-search-forward "\\[\\^\\(?1:[^\]\[]+?\\)\\]:\s\\(?2:.*\\)$\\|\\[\\^\\(?3:[^\]\[]+?\\)\\]\\{1\\}\\|\\[\\(?4:[^\]\[]+?\\)\\]\(#\\(?5:.+?\\)\)\\{1\\}\\|.\\[\\(?6:[^\]\[]+?\\)\\]\(\\(?7:[^#].+?\\)\)\\{1\\}\\|\\[\\(?8:[^\]\[]+?\\)\\]\(\\(?9:.+?\\)\)\\{1\\}" nil t)
            (pcase (match-string-no-properties 0)
              ((pred (lambda (el) (string-match-p "^\\[\\^.+?\\]:\s.*$" el)))
               (replace-match "[fn:\\1] \\2"))

              ((pred (lambda (el) (string-match-p "\\[\\^.+?\\]\\{1\\}" el)))
               (replace-match "[fn:\\3]"))

              ((pred (lambda (el) (string-match-p "\\[.+?\\]\(#.+?\)\\{1\\}" el)))
               (replace-match "[[*\\5][\\4]]"))

              ((pred (lambda (el) (string-match-p "!\\[.*\\]\([^#].*\)" el)))
               (replace-match "[[\\7][\\6]]"))

              ((pred (lambda (el) (string-match-p "[[:blank:]]\\[.*\\]\([^#].*\)" el)))
               (replace-match " [[\\7][\\6]]"))

              ((pred (lambda (el) (string-match-p "\\[.+?\\]\(.+?\)\\{1\\}" el)))
               (replace-match "[[\\9][\\8]]"))))

          (goto-char (point-min))
          (while
              (re-search-forward
               "\\[fn:\\(.+?\\)\\]\\{1\\}" nil t)
            (pcase (match-string 0)
              ((pred (lambda (el) (string-match-p "\\[fn:.+?[[:blank:]].+?\\]\\{1\\}" (substring-no-properties el))))
               (progn
                 (replace-regexp-in-region "[[:blank:]]" "_" (match-beginning 1) (match-end 1))))))))))
    nil))

(defun consult-gh--github-header-to-org (&optional buffer)
  "Convert GitHub's default markdown header to \='org-mode in BUFFER."
  (let ((buffer (or buffer (current-buffer))))
    (with-current-buffer buffer
      (save-match-data
        (save-mark-and-excursion
          (save-restriction
            (goto-char (point-min))
            (when (re-search-forward "^-\\{2\\}$" nil t)
              (delete-char -2)
              (insert "-----\n")
              (while (re-search-backward "\\(^[a-zA-Z0-9._-]+:[[:blank:]]\\)" nil t)
                (replace-match "#+\\1" nil nil)))))))))

(defun consult-gh--markdown-to-org (&optional buffer)
  "Convert from markdown format to \='org-mode format in BUFFER.

This is used for viewing repos \(a.k.a. fetching README file of repos\)
or issue, when `consult-gh-repo-preview-major-mode' or
`consult-gh-issue-preview-major-mode'  is set to \='org-mode."
  (let ((buffer (or buffer (current-buffer))))
    (with-current-buffer buffer
      (consult-gh--markdown-to-org-footnotes buffer)
      (consult-gh--markdown-to-org-emphasis buffer)
      (consult-gh--markdown-to-org-links buffer)
      (org-mode)
      (org-table-map-tables 'org-table-align t)
      (org-fold-show-all)
      (goto-char (point-min))))
  nil)

(defun consult-gh-recenter (&optional pos)
  "Recenter the text in a window so that the cursor is at POS.

POS a symbol and can be \='top, \='bottom or \='middle.
The default is \='middle so if POS is nil or anything else,
the text will be centered in the middle of the window."
  (let ((this-scroll-margin
	 (min (max 0 scroll-margin)
	      (truncate (/ (window-body-height) 4.0))))
        (pos (or pos 'middle)))
    (pcase pos
      ('middle
       (recenter nil t))
      ('top
       (recenter this-scroll-margin t))
      ('bottom
       (recenter (- -1 this-scroll-margin) t))
      (_
       (recenter nil t)))))

(defun consult-gh--org-to-markdown (&optional buffer)
  "Convert content of BUFFER from org format to markdown.

This is used for creating or editing comments, issues, pull requests,
etc. in org format.  It Uses `ox-gfm' for the conversion."
  (when (derived-mode-p 'org-mode)
    (let* ((org-export-with-toc nil)
           (org-export-preserve-breaks t)
           (text (consult-gh--whole-buffer-string buffer)))
      (save-mark-and-excursion
        (with-temp-buffer
          (and (stringp text) (insert text))
          (save-window-excursion (ignore-errors
                                   (org-export-to-buffer 'gfm (current-buffer)))
                                 (buffer-string)))))))

(defun consult-gh--format-text-for-mode (text &optional mode)
  "Format TEXT according to MODE."
  (let* ((mode (or mode major-mode)))
    (when (and text
               (stringp text)
               (not (string-empty-p text)))
      (with-temp-buffer
        (insert text)
        (goto-char (point-min))
        (save-excursion
        (while (re-search-forward "\r\n" nil t)
          (replace-match "\n")))
        (apply #'propertize (pcase mode
                            ('org-mode
                             (consult-gh--markdown-to-org)
                             (consult-gh--whole-buffer-string))
                            (_ (consult-gh--whole-buffer-string)))
             (text-properties-at 0 text))))))

(defun consult-gh--time-ago (datetime)
  "Convert DATETIME to human-radable time difference.

DATETIME must be a time string in the past.
It returns strings like “1 year ago”, “30 minutes ago”."
  (when (stringp datetime) (setq datetime (date-to-time datetime)))
  (let* ((delta (float-time (time-subtract (current-time) datetime)))
         (years (format-seconds "%y" delta))
         (days (and (<= (string-to-number years) 0) (format-seconds "%d" delta)))
         (months (and days (>= (string-to-number days) 30) (number-to-string (/ (string-to-number days) 30))))
         (hours (and days (<= (string-to-number days) 0) (format-seconds "%h" delta)))
         (minutes (and hours (<= (string-to-number hours) 0) (format-seconds "%m" delta)))
         (seconds (and minutes (<= (string-to-number minutes) 0) (format-seconds "%s" delta))))
    (or  (and seconds (concat seconds " second(s) ago"))
         (and minutes (concat minutes " minute(s) ago"))
         (and hours (concat hours " hour(s) ago"))
         (and months (concat months " month(s) ago"))
         (and days (concat days " day(s) ago"))
         (and years (concat years " year(s) ago"))
         "now")))

(defun consult-gh--get-region-with-prop (prop &optional buffer beg end)
  "Get region with property PROP from BUFFER.

When optional arguments BEG and END are no-nil, limit the search between
BEG and END positions."
  (with-current-buffer (or buffer (current-buffer))
    (unless  (= (buffer-size buffer) 0)
    (save-excursion
      (goto-char (or beg (point-min)))
      (let* ((regions nil)
             (begin (point))
             (isProp (get-text-property (point) prop)))
        (while-let ((next (and (< begin (or end (point-max))) (next-single-property-change begin prop nil end))))
          (goto-char next)
          (when (and (get-text-property (- (point) 1) prop) isProp)
            (push (cons (set-marker (make-marker) begin) (point-marker)) regions))
          (setq begin (point))
          (setq isProp (get-text-property (point) prop)))
        (goto-char (or end (point-max)))
        (when (and (get-text-property (- (point) 1) prop) isProp)
          (push (cons (set-marker (make-marker) begin) (point-marker)) regions))
        (nreverse regions))))))

(defun consult-gh--delete-region-with-prop (prop &optional buffer beg end)
  "Remove any text with property PROP from BUFFER.

When optional arguments BEG and END are non-nil, limit the search between
BEG and END positions."

  (let ((regions (consult-gh--get-region-with-prop prop buffer beg end)))
    (when (and regions (listp regions))
      (cl-loop for region in regions
               do
               (let ((p1 (car region))
                     (p2 (min (cdr region) (point-max))))
               (delete-region p1 p2))))))

(defun consult-gh--hide-region-with-prop (prop &optional buffer beg end symbol)
  "Hide any text with property PROP from BUFFER.

When optional arguments BEG and END are non-nil, limit the search between
BEG and END positions.
When SYMBOL is non-nil, set the property \='invisible to SYMBOL,
otherwise set it to \='consult-gh"

  (let ((regions (consult-gh--get-region-with-prop prop buffer beg end)))
    (when (and regions (listp regions))
      (cl-loop for region in regions
               do
               (let ((p1 (car region))
                     (p2 (min (cdr region) (point-max))))
               (put-text-property p1 p2 'invisible (or symbol 'consult-gh)))))))

(defun consult-gh--unhide-region-with-prop (prop &optional buffer beg end)
  "Unhide any text with property PROP from BUFFER.

Removes the property \='invisible from regions with PROP.

When optional arguments BEG and END are non-nil, limit the search between
BEG and END positions."

  (let ((regions (consult-gh--get-region-with-prop prop buffer beg end)))
    (when (and regions (listp regions))
      (cl-loop for region in regions
               do
               (let ((p1 (car region))
                     (p2 (min (cdr region) (point-max))))
               (remove-list-of-text-properties p1 p2 '(invisible)))))))

(defun consult-gh--get-region-with-overlay (symbol &optional buffer beg end)
  "Get regions with SYMBOL overlay from BUFFER.

When BEG and END are non-nil, look in the region between
BEG and END positions."
  (with-current-buffer (or buffer (current-buffer))
    (let ((points nil))
    (save-excursion
      (dolist (o (overlays-in (or beg (point-min)) (or end (point-max))))
        (when (overlay-get o symbol)
          (push (cons (overlay-start o) (overlay-end o)) points))))
    points)))

(defun consult-gh--delete-region-with-overlay (symbol &optional buffer beg end)
  "Remove regions with SYMBOL overlay from BUFFER.

When BEG or END are non-nil, limit the search in the region between
BEG and END positions."
  (with-current-buffer (or buffer (current-buffer))
    (save-excursion
      (dolist (o (overlays-in (or beg (point-min)) (or end (point-max))))
        (when (overlay-get o symbol)
          (delete-region (overlay-start o) (overlay-end o)))))))

(defun consult-gh--separate-add-and-remove (new old)
  "Compare the lists NEW and OLD and return a list of differences.

Splits the difference and returns a list where:
 The first element is a list of items to add to OLD
 The second element is a list of items to remove form OLD."
  (cond
   ((and (listp new) (listp old) (not (equal new old)))
   (list
    (seq-uniq (seq-difference new old))
    (seq-uniq (seq-difference old new))))
   (t
    (list nil nil))))

(defun consult-gh--list-to-string (list)
  "Convert a LIST of strings to a single comma separated string.

If any string in LIST contains comma, wrap it in quotes."
  (save-match-data
  (mapconcat (lambda (item) (substring-no-properties
                             (if (string-match (format ".*,.*" ) item)
                                 (cond
                                  ((string-match "\".*\"" item) item)
                                  (t (format "\"%s\"" item)))
                                            item)))
             list
             ",")))

(defun consult-gh-url-copy-file (url newname)
  "Copy URL to NEWNAME.

Both arguments must be strings."
  (let* ((inhibit-message t))
    (url-retrieve url
                  (lambda (_)
                     (let* ((handle (mm-dissect-buffer t)))
                       (let ((mm-attachment-file-modes (default-file-modes)))
                         (mm-save-part-to-file handle newname))
                       (mm-destroy-parts handle)
                       (kill-buffer (current-buffer))))))
    nil)

(defun consult-gh--read-local-file (&optional files prompt initial require-match default-dir)
  "Read file name from FILES.

When FILES is nil, read file from file-system.
PROMPT, INITIAL, and REQUIRE-MATCH are passed to `consult--read'.
when DEFAULT-DIR is non-nil, set `default-directory' to
DEFAULT-DIR before querying for files."
  (let ((default-directory (or default-dir default-directory)))
  (consult--read (or files
                     (completion-table-in-turn #'completion--embedded-envvar-table
                                           #'completion--file-name-table))
                 :prompt (or prompt "Select File: ")
                 :require-match require-match
                 :category 'file
                 :initial initial
                 :lookup (lambda (sel _cands &rest _args)
                           (file-truename sel))
                 :state (lambda (action cand)
                          (let ((preview (consult--buffer-preview)))
                            (pcase action
                              ('preview
                               (if cand
                                   (when (and (file-exists-p cand)
                                              (not (file-directory-p cand)))
                                     (let* ((filesize (file-attribute-size
                                                        (file-attributes cand)))
                                            (filesize (or (and (numberp filesize)
                                                               (float filesize))
                                                          0))
                                            (not-large (<= filesize large-file-warning-threshold)))
                                       (if not-large
                                           (funcall preview action
                                                    (find-file-noselect (file-truename cand))))))))
                              ('return
                               cand))))
                          :preview-key consult-gh-preview-key)))

;;; Backend functions for call to `gh` program

(defun consult-gh--auth-account-host (&optional account)
  "Get the host of current ACCOUNT."
  (let* ((account (or account consult-gh--auth-current-account)))
    (when (consp account)
      (cadr account))))

;;;###autoload
(defmacro consult-gh-with-host (host &rest body)
  "Run BODY after setting environment var “GH_HOST” to HOST."
  `(progn
     (if ,host
         (with-environment-variables
             (("GH_HOST" ,host))
           ,@body)
       ,@body)))

(cl-defun consult-gh--make-process (name &rest args &key filter when-done on-error cmd-args)
  "Make asynchronous process with NAME and pass ARGS to “gh” program.

This command runs gh program asynchronously.

Description of Arguments:
  NAME      a string; is passed as \=:name t `make-process'
  FILTER    a function: iss passed as \=:filter to `make-process'
  WHEN-DONE a function; is applied to the the output of process when it is done
            This function should take two input arguments STATUS and STRING
            STATUS is the status of the process and STRING is the output
  ON-ERROR  a function; is applied to the the output of process when the process
            is killed or fails.  This function should take two input arguments
            STATUS and STRING.  STATUS is the status of the process and STRING
            is the output.
  CMD-ARGS  a list of strings; is passed as \=:command to `make-process'"
  (if (executable-find "gh")
      (consult-gh-with-host
       (consult-gh--auth-account-host)
       (when-let* ((proc (get-process name)))
         (delete-process proc))
       (let* ((cmd-args (append (list "gh") cmd-args))
              (proc-buf (generate-new-buffer (concat consult-gh--async-process-buffer-name "-" name)))
              (when-done (if (functionp when-done)
                             when-done
                           (lambda (_ str) str)))
              (proc-sentinel
               (lambda (proc event)
                 (cond
                  ((string-prefix-p "finished" event)
                   (with-current-buffer proc-buf
                     (widen)
                     (when when-done
                       (funcall when-done event (buffer-string)))
                     (erase-buffer)))
                  ((string-prefix-p "killed" event)
                   (if on-error
                       (with-current-buffer proc-buf
                         (widen)
                         (funcall on-error event (buffer-string)))
                     (message "%s was %s" (process-name proc) (add-text-properties 0 6 (list  'face 'warning) event))))
                  (t
                   (if on-error
                       (with-current-buffer proc-buf
                         (widen)
                         (funcall on-error event (buffer-string)))
                     (message "%s failed: %s " (process-name proc) (add-text-properties 0 6 (list  'face 'warning) event)))))
                 (when (> (buffer-size proc-buf) 0)
                   (with-current-buffer (get-buffer-create consult-gh--async-log-buffer)
                     (goto-char (point-max))
                     (insert (format ">>>>> stderr (%s) >>>>>\n" (process-name proc)))
                     (let ((beg (point)))
                       (insert-buffer-substring proc-buf)
                       (save-excursion
                         (goto-char beg)
                         (unless on-error
                           (message #("%s" 0 2 (face error))
                                    (buffer-substring-no-properties (pos-bol) (pos-eol))))))
                     (insert (format "<<<<< stderr (%s) <<<<<\n" (process-name proc)))))))
              (process-adaptive-read-buffering t))
         (with-current-buffer proc-buf
           (set-buffer-file-coding-system 'unix))
         (consult-gh--async-log "consult-gh--make-process %s started %s\n" name cmd-args)
         (make-process :name name
                       :buffer proc-buf
                       :noquery t
                       :command cmd-args
                       :connection-type 'pipe
                       :filter filter
                       :sentinel proc-sentinel)))
    (progn
      (user-error (propertize "\"gh\" is not found on this system" 'face 'warning))
      nil)))

(defun consult-gh--call-process (&rest args)
  "Run “gh” program and pass ARGS as arguments.

Returns a list where the CAR is exit status
\(e.g. 0 means success and non-zero means error\) and CADR is the output's text.
If gh is not found it returns \(127 “”\)
and a message saying “gh” is not found."
  (if (executable-find "gh")
      (with-temp-buffer
        (set-buffer-file-coding-system 'unix)
        (consult-gh-with-host (consult-gh--auth-account-host)
                              (list (apply #'call-process "gh" nil (current-buffer) nil args)
                                    (buffer-string))))
    (progn
      (user-error (propertize "\"gh\" is not found on this system" 'face 'warning))
      '(127 ""))))

(defun consult-gh--command-to-string (&rest args)
  "Run `consult-gh--call-process' and return a string if no error.

If there are errors passes them to `message'.
ARGS are passed to `consult-gh-call-process'"
  (let ((out (apply #'consult-gh--call-process args)))
    (if (= (car out) 0)
        (cadr out)
      (progn
        (message (cadr out))
        nil))))

(defun consult-gh--api-get-json (url &rest args)
  "Make a GitHub API call to get response in JSON format.

Passes the URL \(e.g. a GitHub API URL\), as well as ARGS to
“gh api” command."
  (let ((args (append `("api" "-H" "Accept: application/vnd.github+json" "--paginate" ,url) (if (listp args) args (list args)))))
  (apply #'consult-gh--call-process args)))

(defun consult-gh--api-get-command-string (url &rest args)
  "Return the output of an api call with “get” method to URL with ARGS.

Passes the ARGS to a GitHub API URL using
“gh api -H Accept:application/vnd.github+json --method GET URL ARGS” command."
  (let ((args (append `("api" "-H" "Accept: application/vnd.github+json" "--paginate" ,url) (if (listp args) args (list args)))))
  (apply #'consult-gh--command-to-string args)))

(defun consult-gh--api-put-command-string (url &rest args)
  "Return the output of an api call with “put” method to URL with ARGS.

Passes the ARGS to a GitHub API URL using
“gh api -H Accept:application/vnd.github+json --method PUT URL ARGS” command."
  (let ((args (append `("api" "-H" "Accept: application/vnd.github+json" ,url) (list "--method" "PUT") (if (listp args) args (list args)))))
  (apply #'consult-gh--command-to-string args)))

(defun consult-gh--json-to-hashtable (json &optional keys)
  "Convert a JSON object to a hash table.

Uses lists for arrays and symbols for keys.
If optional argument KEYS is non-nil, returns only the value of KEYS."
  (if (stringp json)
      (let* ((json-object-type 'hash-table)
            (json-array-type 'list)
            (json-key-type 'keyword)
            (json-false :false)
            (results (json-read-from-string json)))
        (cond
         ((hash-table-p results)
          (cond
           ((and keys (listp keys))
            (let* ((table (make-hash-table :test 'equal)))
              (cl-loop for key in keys
                     do
                     (puthash key (gethash key results) table))
            table))
          ((and keys (symbolp keys))
          (gethash keys results))
          (t results)))
         ((listp results)
          (cond
           ((and keys (listp keys))
              (cl-loop for result in results
                     collect
                     (let* ((table (make-hash-table :test 'equal)))
                       (cl-loop for key in keys
                              do (puthash key (gethash key result) table))
                              table)))
          ((and keys (symbolp keys))
           (cl-loop for result in results
                    collect
                    (gethash keys result)))
          (t results)))))
    nil))

(defun consult-gh--group-function (cand transform &optional group-by)
  "Group CAND by GROUP-BY keyword.

This is passed as GROUP to `consult--read' on candidates
and is used to define the grouping for CAND.

If TRANSFORM is non-nil, the CAND itself is returned."
  (if transform (substring cand)
    (let* ((group-by (or consult-gh--override-group-by group-by consult-gh-group-by))
           (group-by (if (stringp group-by) (if (not (keywordp (intern group-by))) (intern (concat ":" (format "%s" group-by))) (intern group-by)) group-by)))
      (cond
       ((member group-by '(nil :nil :none :no :not))
        nil)
       ((not (member group-by '(:t t)))
        (if-let* ((group (get-text-property 0 group-by cand)))
            (format "%s" group)
          "N/A"))
       (t t)))))

(defun consult-gh--split-command (input)
  "Return command argument and options list given INPUT string.

It sets `consult-gh--override-group-by' if and argument
for grouping is provided in options.

See `consult--command-split' for more info."
  (pcase-let* ((`(,query . ,opts) (consult--command-split input)))
    (if (and opts (listp opts) (> (length opts) 0))
        (progn
          (setq opts (cl-substitute ":group" ":g" opts :test 'equal))
          (if (member ":group" opts)
              (progn
                (setq consult-gh--override-group-by (cadr (member ":group" opts)))
                (setq opts (seq-difference opts (list ":group" (cadr (member ":group" opts))))))
            (setq consult-gh--override-group-by nil)))
      (setq consult-gh--override-group-by nil))
    (append (list (or query input)) opts)))

(defun consult-gh--get-current-username ()
  "Get the currently logged in user.

Runs “gh api user” and returns the login field of json data."
  (consult-gh--json-to-hashtable (cadr (consult-gh--api-get-json "user")) :login))

(defun consult-gh--get-current-user-orgs (&optional user include-user)
  "Get the organizations for USER.

USER defaults to currently logged in user.
When INCLUDE-USER is non-nil, add the name of the user to the list."
  (let* ((data (if user (consult-gh--api-get-json (format "users/%s/orgs" user)) (consult-gh--api-get-json "user/orgs")))
         (table (when (eq (car data) 0)
                  (consult-gh--json-to-hashtable (cadr data) :login)))
         (user (or user (consult-gh--get-current-username))))
    (cond
     ((listp table)
      (append table (if include-user (list user))))
     ((stringp table)
      (append (list table)
              (if include-user (list user))))
     (t (if include-user (list user))))))

(defun consult-gh--get-user-template-repos (&optional user)
  "List template repository for USER.

When USER is nil, the current authenticated user is used instead."
  (let ((endpoint (if user (format "users/%s/repos" user) "user/repos")))
    (delq nil (mapcar (lambda (item) (when (eq (gethash :is_template item) t)
                                       (gethash :full_name item)))
                      (consult-gh--json-to-hashtable
                       (cadr
                        (consult-gh--api-get-json endpoint)))))))

(defun consult-gh--get-repo-from-directory (&optional dir)
  "Return the full name of the GitHub repository in current directory.

If optional arg DIR is non-nil, use DIR instead of the current directory.
Formats the output as “[HOST/]OWNER/REPO” if any, otherwise returns nil."
  (let* ((default-directory (or dir default-directory))
         (response (consult-gh--call-process "repo" "view" "--json" "nameWithOwner" "--jq" ".nameWithOwner")))
    (if (eq (car response) 0)
        (if (not (string-empty-p (cadr response)))
            (string-trim (cadr response))
          nil)
      nil)))

(defun consult-gh--get-repo-from-topic (&optional topic)
  "Return the full name of the GitHub repository in topic.

TOPIC should be a string with property field :repo, and defaults to
`consult-gh--topic'."
  (when-let* ((topic (or topic consult-gh--topic)))
    (if (stringp topic)
        (get-text-property 0 :repo topic))))

(defun consult-gh--async-log (formatted &rest args)
  "Log FORMATTED ARGS to variable `consult-gh--async-log-buffer'.

FORMATTED and ARGS are passed to `format' with \=(format FORMATTED ARGS)"
  (with-current-buffer (get-buffer-create consult-gh--async-log-buffer)
    (goto-char (point-max))
    (insert (apply #'format formatted args))))

;;; Backend functions for internal consult-gh use

(defun consult-gh--split-repo (repo &optional separators)
  "Split REPO string by SEPARATORS to get user and package name.

Returns a list where CAR is the user's name and CADR is the package name."
  (let ((separators (or separators "\/")))
    (and (stringp repo) (split-string repo separators))))

(defun consult-gh--get-username (repo)
  "Return the username of REPO.

\(e.g. “armindarvish” if REPO is “armindarvish/consult-gh”\)"
  (car (consult-gh--split-repo repo)))

(defun consult-gh--get-package (repo)
  "Return the package name of REPO.

\(e.g. “consult-gh” if REPO is “armindarvish/consult-gh”\)"
  (cadr (consult-gh--split-repo repo)))

(defun consult-gh--get-user-info (&optional user)
  "Get the contact of USER from GitHub API."
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((user (or user (consult-gh--get-current-username)))
          (json (consult-gh--api-get-json (format "users/%s" user))))
     (if (eq (car json) 0)
         (consult-gh--json-to-hashtable (cadr json) '(:name :email :html_url))
       user))))

(defun consult-gh--get-user-fullname (&optional user)
  "Get the name of USER from GitHub API."
  (gethash :name (consult-gh--get-user-info user)))

(defun consult-gh--get-user-email (&optional user)
  "Get the email of USER from GitHub API."
  (gethash :email (consult-gh--get-user-info user)))

(defun consult-gh--get-user-link (&optional user)
  "Get the link to USER page on GitHub."
  (gethash :html_url (consult-gh--get-user-info user)))

(defun consult-gh--user-canadmin (repo)
  "Determine if the current user can administer REPO."
  (let ((json (consult-gh--command-to-string "repo" "view" repo "--json" "viewerCanAdminister")))
    (and (stringp json)
         (eq (consult-gh--json-to-hashtable json :viewerCanAdminister) 't))))

(defun consult-gh--user-canwrite (repo)
  "Determine if the current user have write premission in REPO."
  (let* ((json (consult-gh--command-to-string "repo" "view" repo "--json" "viewerPermission"))
        (permission (and (stringp json) (consult-gh--json-to-hashtable json :viewerPermission))))
    (or (equal permission "WRITE") (equal permission "ADMIN"))))

(defun consult-gh--user-isauthor (topic &optional user)
  "Determine if the USER is the author of TOPIC.

USER defaults to `consult-gh--auth-current-active-account'."
  (let* ((user (or user (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (type (get-text-property 0 :type topic))
         (repo (get-text-property 0 :repo topic))
         (number (get-text-property 0 :number topic))
         (author (get-text-property 0 :author topic))
         (json (when (and (not author) number (member type '("issue" "pr")))
                 (consult-gh--command-to-string type "view" number "--repo" repo "--json" "author")))
         (table (and (stringp json) (consult-gh--json-to-hashtable json :author)))
         (author (or author (and (hash-table-p table) (gethash :login table)))))
         (equal user author)))

(defun consult-gh--tempdir ()
 "Make a new temporary directory with timestamp."
 (if (and consult-gh--current-tempdir (stringp consult-gh--current-tempdir) (< (time-convert (time-subtract (current-time) (nth 5 (file-attributes (substring (file-name-as-directory consult-gh--current-tempdir) 0 -1)))) 'integer) consult-gh-temp-tempdir-cache))
         consult-gh--current-tempdir
(expand-file-name (make-temp-name (concat (format-time-string consult-gh-temp-tempdir-time-format  (current-time)) "-")) consult-gh-tempdir)))

(defun consult-gh--parse-diff (diff)
  "Parse DIFF to extract diff hunks per file.

Returns an alist with key value pairs of (file . diff)"
  (let ((chunks nil)
        (p nil))
    (with-temp-buffer
      (save-match-data
        (insert diff)
        (goto-char (point-max))
        (setq p (point))
        (while (re-search-backward "^--- \\(?1:.*\\)\n\\+\\+\\+ \\(?2:.*\\)\n\\|similarity index.*\nrename from \\(?1:.*\\)\nrename to \\(?2:.*\\)\n" nil t)
          (let ((filea (match-string 1))
                (fileb (match-string 2))
                (start (or (min (+ (match-end 2) 1) (point-max)) (point)))
                (file nil))
            (when filea
              (if (equal filea "/dev/null") (setq filea nil) (setq filea (string-trim-left filea "a/"))))
            (when fileb
              (if (equal fileb "/dev/null") (setq fileb nil) (setq fileb (string-trim-left fileb "b/"))))
            (cond
             ((looking-at "similarity index.*") (setq file (propertize (concat "renamed\t" filea "->" fileb) :path fileb)))
             ((and filea fileb (setq file (propertize (concat "modified\t" filea) :path filea))))
             (fileb (setq file (propertize (concat "new file\t" fileb) :path fileb)))
             (filea (setq file (propertize (concat "deleted\s\t" filea) :path filea))))
            (when (looking-at "---") (push (cons file (buffer-substring start p)) chunks))
            (when (looking-at "similarity index.*") (push (cons file nil) chunks))
            (re-search-backward "diff --git" nil t)
            (setq p (max (- (point) 1) (point-min)))))))
    chunks))

(defun consult-gh--get-line-and-side-at-pos-inside-diff (&optional pos)
  "Get line and side at POS inside a diff code block."
  (save-mark-and-excursion
    (when pos (goto-char pos))
    (when (plist-get (get-text-property (point) :consult-gh) :code)
      (pcase-let* ((`(,_end ,endline) (list (point) (line-number-at-pos)))
                   (`(,begin ,beginline) (save-excursion
                                           (re-search-backward "@@ [-\\+]\\(?1:[0-9]+\\).* \\+\\(?2:[0-9]+\\).*" nil t)
                                           (list (point) (line-number-at-pos))))
                   (cord (list (if (and (match-string 1) (> (string-to-number (match-string 1)) 0))
                                   (string-to-number (match-string 1))
                                 0)
                               (if (and (match-string 2) (> (string-to-number (match-string 2)) 0))
                                   (string-to-number (match-string 2))
                                 0)))

                   (difference (- endline beginline 1))
                   (count 0))
        (goto-char (pos-bol))
        (cond
         ((= endline beginline) (list nil nil))
         ((looking-at "-.*")
          (while (re-search-backward "^+.*" begin t) (cl-incf count))
          (list "LEFT" (- (+ (car cord) difference) count)))

         ((looking-at "+.*")
          (while (re-search-backward "^-.*" begin t) (cl-incf count))
          (list "RIGHT" (- (+ (cadr cord) difference) count)))

         (t
          (while (re-search-backward "^-.*" begin t) (cl-incf count))
          (list "RIGHT" (- (+ (cadr cord) difference) count))))))))

(defun consult-gh--get-line-and-side-inside-diff ()
  "Get lines and side from region or point."
  (pcase-let* ((`(,start ,end) (if (region-active-p)
                                   (list (region-beginning)
                                         (region-end))
                                 (list nil (point)))))
    (cond
     ((and start end)
      (let ((l1 (consult-gh--get-line-and-side-at-pos-inside-diff start))
            (l2  (consult-gh--get-line-and-side-at-pos-inside-diff end)))
        (cond
         ((> (cadr l2) (cadr l1))
            (append l2 l1))
         ((> (cadr l1) (cadr l2))
            (append l1 l2))
         (t l2))))
     (end (consult-gh--get-line-and-side-at-pos-inside-diff end))
     (t
      (consult-gh--get-line-and-side-at-pos-inside-diff)))))

(defun consult-gh--get-gitignore-template-list ()
  "List name of .gitignore templates."
  (string-split (string-trim
                 (cadr
                  (consult-gh--api-get-json "gitignore/templates"))
                 "\\[" "\\]") "," t "\""))

(defun consult-gh--get-gitignore-template-content (template)
  "Get content of gitignore TEMPLATE."
  (when-let* ((response (consult-gh--command-to-string "api" (format "gitignore/templates/%s" template))))
    (gethash :source (consult-gh--json-to-hashtable response))))

(defun consult-gh--gitignore-template-preview (action cand)
  "Preview function for gitignore templates.

This is passed as state function to `consult--read'.

For more details on ACTION and CAND refer to docstring of
`consult--with-preview'."
  (let* ((preview (consult--buffer-preview)))
  (pcase action
    ('preview
     (if (and consult-gh-show-preview cand (stringp cand))
         (let* ((content (consult-gh--get-gitignore-template-content cand))
                (buff (get-buffer-create (format "*consult-gh-preview: gitignore-%s*" cand))))
           (with-current-buffer buff
             (erase-buffer)
             (insert content)
             (set-buffer-file-coding-system 'utf-8)
             (set-buffer-multibyte t))
           (add-to-list 'consult-gh--preview-buffers-list buff)
           (funcall preview action
                    buff))))
    ('return
     cand))))

(defun consult-gh--read-gitignore-template (&optional templates)
"Read a gitignore template from TEMPLATES with preview.

TEMPLATES default to the list of templates from github:
URL `https://github.com/github/gitignore'"
  (consult--read (or templates (consult-gh--get-gitignore-template-list))
                 :prompt (format "Select template for %s" (propertize ".gitignore " 'face 'error))
                 :state #'consult-gh--gitignore-template-preview
                 :require-match t
                 :history 'consult-gh--gitignore-templates-history
                 :preview-key consult-gh-preview-key
                 :sort nil))

(defun consult-gh--repo-has-discussions-enabled-p (repo)
  "Check if REPO has discussions enabled."
  (gethash :hasDiscussionsEnabled (consult-gh--json-to-hashtable (consult-gh--command-to-string "repo" "view" repo "--json" "hasDiscussionsEnabled"))))

(defun consult-gh--repo-has-issues-enabled-p (repo)
  "Check if REPO has issues enabled."
(gethash :hasIssuesEnabled (consult-gh--json-to-hashtable (consult-gh--command-to-string "repo" "view" repo "--json" "hasIssuesEnabled"))))

(defun consult-gh--repo-has-projects-enabled-p (repo)
  "Check if REPO has projects enabled."
(gethash ::hasProjectsEnabled (consult-gh--json-to-hashtable (consult-gh--command-to-string "repo" "view" repo "--json" "hasProjectsEnabled"))))

(defun consult-gh--get-milestones (repo)
  "Get a list of milestones in REPO."
  (let* ((topic consult-gh--topic)
         (json (consult-gh--command-to-string "repo" "view" repo "--json" "milestones"))
         (table (and (stringp json) (consult-gh--json-to-hashtable json :milestones)))
         (milestones (and table (listp table) (mapcar (lambda (item) (gethash :title item)) table))))
    (when (stringp topic)
      (add-text-properties 0 1 (list :valid-milestones milestones) topic))
    milestones))

(defun consult-gh--get-assignable-users (repo)
  "Get a table of assignbale users of REPO."
  (let* ((topic consult-gh--topic)
         (json (consult-gh--command-to-string "repo" "view" repo "--json" "assignableUsers"))
         (table (and (stringp json)  (consult-gh--json-to-hashtable json :assignableUsers)))
         (users (and table (listp table) (mapcar (lambda (item) (gethash :login item)) table))))
    (when (stringp topic)
      (add-text-properties 0 1 (list :assignable-users users) topic))
    users))

(defun consult-gh--get-mentionable-users (repo)
  "Get a table of mentionable users of REPO."
  (let* ((topic consult-gh--topic)
         (json (consult-gh--command-to-string "repo" "view" repo "--json" "mentionableUsers"))
         (table (and (stringp json) (consult-gh--json-to-hashtable json :mentionableUsers)))
         (users (and table (listp table) (mapcar (lambda (item) (gethash :login item)) table))))

(when (stringp topic)
  (add-text-properties 0 1 (list :mentionable-users users) topic))
users))

(defun consult-gh--get-labels (repo)
  "Get a list of labels in REPO."
  (let* ((topic consult-gh--topic)
         (json (consult-gh--command-to-string "repo" "view" repo "--json" "labels"))
         (table (and (stringp json) (consult-gh--json-to-hashtable json :labels)))
         (labels (and table (listp table) (mapcar (lambda (item) (gethash :name item)) table))))
    (when (stringp topic)
      (add-text-properties 0 1 (list :valid-labels labels) topic))
    labels))

(defun consult-gh--get-projects (repo)
  "Get a list of projects of REPO."
  (let* ((topic consult-gh--topic)
         (json (consult-gh--command-to-string "repo" "view" repo "--json" "projectsV2"))
         (table (and (stringp json) (consult-gh--json-to-hashtable json :projectsV2)))
         (nodes (and (hash-table-p table) (gethash :Nodes table)))
         (projects (and nodes (listp nodes) (mapcar
                                             (lambda (item) (gethash :title item))
                                             nodes))))
    (when (stringp topic)
      (add-text-properties 0 1 (list :valid-projects projects) topic)
      projects)))

(defun consult-gh--get-discussion-categories (repo)
  "Get a list of discusssion categories of REPO."
(when (consult-gh--repo-has-discussions-enabled-p repo)
  (let*  ((query (format "query={repository (owner: \"%s\", name: \"%s\")
{discussionCategories(first:100){ nodes { name } }}}"
                         (consult-gh--get-username repo)
                         (consult-gh--get-package repo)))
          (table (consult-gh--json-to-hashtable (consult-gh--api-get-command-string "graphql" "-f" query) :data)))
         (and (hash-table-p table)
              (mapcar (lambda (item)
                        (when (hash-table-p item)
                          (gethash :name item)))
                      (map-nested-elt table '(:repository :discussionCategories :nodes)))))))

(defun consult-gh--enable-keybindings-alist (map alist)
  "Enable keymap ALIST in MAP."
  (cl-loop for k in alist
           do
           (keymap-set map (car k) (cdr k))))

(defun consult-gh--disable-keybindings-alist (map alist)
  "Disable keymap ALIST in MAP."
  (cl-loop for k in alist
           do
           (keymap-unset map (car k) t)))

(defun consult-gh--get-split-style-character (&optional style)
  "Get the character for consult async split STYLE.

STYLE defaults to `consult-async-split-style'."
  (let* ((style (or style consult-async-split-style 'none))
         (spec (alist-get style consult-async-split-styles-alist))
         (char (or (plist-get spec :initial) (plist-get spec :separator))))
    (if (characterp char)
        (char-to-string char)
      "")))

(defun consult-gh--get-license-list ()
  "List name of open source license keys.

Each item is a cons of (name . key) for a license."
  (mapcar (lambda (item) (cons (gethash :name item) (gethash :key item)))
          (consult-gh--json-to-hashtable
           (cadr
            (consult-gh--api-get-json "licenses")))))

(defun consult-gh--get-license-content (license)
  "Get body of LICENSE."
  (when-let* ((response (consult-gh--command-to-string "api" (format "licenses/%s" license))))
    (gethash :body (consult-gh--json-to-hashtable response))))

(defun consult-gh--license-preview (action cand)
  "Preview function for license templates.

This is passed as state function to `consult--read'.

For more details on ACTION and CAND refer to docstring of
`consult--with-preview'."
  (let* ((preview (consult--buffer-preview)))
  (pcase action
    ('preview
     (if (and consult-gh-show-preview cand (stringp cand))
         (let* ((content (consult-gh--get-license-content cand))
                (buff (get-buffer-create (format "*consult-gh-preview: license-%s*" cand))))
           (with-current-buffer buff
             (erase-buffer)
             (insert content)
             (set-buffer-file-coding-system 'utf-8)
             (set-buffer-multibyte t))
           (add-to-list 'consult-gh--preview-buffers-list buff)
           (funcall preview action
                    buff))))
    ('return
     cand))))

(defun consult-gh--read-license-key (&optional licenses)
  "Read a license key from LICENSES with preview.

LICENSES default to the list of licenses from github api:
URL `https://docs.github.com/en/rest/licenses'"
  (consult--read (or licenses (consult-gh--get-license-list))
                 :prompt (format "Select a %s" (propertize "license key" 'face 'consult-gh-description))
                 :state #'consult-gh--license-preview
                 :require-match t
                 :history 'consult-gh--license-key-history
                 :lookup #'consult--lookup-cdr
                 :preview-key consult-gh-preview-key
                 :sort nil))

(defun consult-gh--format-view-buffer (&optional type buffer)
  "Format the content of the BUFFER according to TYPE.

TYPE can be “repo”, “issue”, “pr”, or “release”.
uses variables like `consult-gh-issue-preview-major-mode' to set the
major mode and format the contents."
  (with-current-buffer (or buffer (current-buffer))
    (let* ((type (or type "issue"))
           (mode (pcase type
                   ("repo" consult-gh-repo-preview-major-mode)
                   ("issue" consult-gh-issue-preview-major-mode)
                   ("pr" consult-gh-issue-preview-major-mode)
                   ("release" consult-gh-release-preview-major-mode)
                   ("workflow" consult-gh-workflow-preview-major-mode)
                   ("run" consult-gh-run-preview-major-mode)
                   ("commit" consult-gh-commit-preview-major-mode))))
  (save-excursion
    (pcase mode
      ('gfm-mode
       (gfm-mode)
       (when (display-images-p)
         (markdown-display-inline-images)))
      ('markdown-mode
       (markdown-mode)
       (when (display-images-p)
         (markdown-display-inline-images)))
      ('org-mode
       (let ((org-display-remote-inline-images 'download))
         (consult-gh--markdown-to-org)))
      (_
       (consult-gh--markdown-to-org-emphasis)
       (outline-mode))))
  (goto-char (point-min))
  (save-excursion
    (while (re-search-forward "\r\n" nil t)
      (replace-match "\n")))
  (ansi-color-apply-on-region (point-min) (point-max))
  (set-buffer-file-coding-system 'utf-8)
  (set-buffer-multibyte t))))

(defun consult-gh--get-user-tooltip (user &rest _args)
  "Make tooltip for USER."
  (let* ((dir (expand-file-name (format "users/%s/" user) consult-gh-tempdir))
         (_ (unless (file-exists-p dir)
             (make-directory (file-name-directory dir) t)))
         (image-path (expand-file-name "avatar.png" dir))
         (profile-path (expand-file-name "userprofile" dir)))
    (unless (file-exists-p profile-path)
      (consult-gh--make-process (format "consult-gh-user-tooltip-%s" user)
                                :cmd-args (list "api" (format "users/%s" user) "-H" "Accept:application/vnd.github.diff")
                                :when-done (lambda (_ str)
                                           (let* ((inhibit-message t)
                                                  (table (consult-gh--json-to-hashtable str '(:avatar_url :name :email :location :bio)))
                                                  (image-url (and (hash-table-p table)
                                                                  (gethash :avatar_url table)))
                                                  (name (and (hash-table-p table)
                                                             (gethash :name table)))
                                                  (email (and (hash-table-p table)
                                                              (gethash :email table)))
                                                  (loc (and (hash-table-p table)
                                                            (gethash :location table)))
                                                  (bio (and (hash-table-p table)
                                                            (gethash :bio table)))
                                                  (profile-text (concat (propertize user 'face 'consult-gh-user)
                                                               (if name (concat "\n" name))
                                                               (if email (concat "\n" (propertize email 'face 'consult-gh-date)))
                                                               (if loc (concat "\n" (propertize loc 'face 'consult-gh-repo)))
                                                               (if bio (concat "\n" (propertize bio 'face 'consult-gh-description))))))
                                             (unless (file-exists-p image-path)
                                               (consult-gh-url-copy-file image-url image-path))
                                             (with-temp-file profile-path
                                               (prin1 profile-text
                                                      (current-buffer)))
                                             nil))))

    (let* ((image (create-image image-path nil nil :height (floor (* (frame-width) 0.25)))))

      (concat (if (and (file-exists-p image-path) (display-images-p))
                  (concat (propertize " " 'display image) " ")
                consult-gh-user-icon)
              (if (file-exists-p profile-path) (with-temp-buffer (insert-file-contents profile-path)
                                                                 (goto-char (point-min))
                                                                 (read (current-buffer)))
                user)))))

(defun consult-gh--get-repo-tooltip (repo &rest _args)
  "Make tooltip for REPO."
  (let* ((dir (expand-file-name (format "repos/%s/" repo) consult-gh-tempdir))
         (_ (unless (file-exists-p dir)
              (make-directory (file-name-directory dir) t)))
         (image-path (expand-file-name "opengraphimage.png" dir))
         (profile-path (expand-file-name "repoprofile" dir)))
    (unless (file-exists-p profile-path)
      (let* ((query (format "query={
  repository(owner: \"%s\", name: \"%s\") {
    openGraphImageUrl
    visibility
    nameWithOwner
    languages(first:100) { nodes {name}}
    stargazerCount
    updatedAt
    description
  }}" (consult-gh--get-username repo)
  (consult-gh--get-package repo))))
        (consult-gh--make-process (format "consult-gh-repo-tooltip-%s" repo)
                                  :cmd-args (list "api" "-H" "Accept:application/vnd.github.diff" "graphql" "-f" query)
                                  :when-done (lambda (_ str)
                                                (let* ((inhibit-message t)
                                                       (table (consult-gh--json-to-hashtable str :data))
                                                       (table (and (hash-table-p table) (gethash :repository table)))
                                                       (image-url (and (hash-table-p table)
                                                                        (gethash :openGraphImageUrl table)))
                                                       (desc (and (hash-table-p table)
                                                                  (gethash :description table)))
                                                       (name (and (hash-table-p table)
                                                                  (gethash :nameWithOwner table)))
                                                       (vis (and (hash-table-p table)
                                                                 (gethash :visibility table)))
                                                       (langs (and (hash-table-p table)
                                                                   (map-nested-elt table '(:languages :nodes))))
                                                       (langs  (and (listp langs)
                                                                    (mapconcat (lambda(item) (gethash :name item)) langs ", ")))
                                                       (stars (and (hash-table-p table)
                                                                   (gethash :stargazerCount table)))
                                                       (updated (and (hash-table-p table)
                                                                     (gethash :updatedAt table)))
                                                       (profile-text (concat
                                                                      consult-gh-repo-icon
                                                                      "\s"
                                                                      (propertize name 'face 'consult-gh-repo)
                                                                      "\s\s"
                                                                      (propertize vis 'face 'consult-gh-visibility)
                                                                      "\n"
                                                                      (propertize desc 'face 'consult-gh-description)
                                                                      "\n"
                                                                      (propertize langs 'face 'consult-gh-pr)
                                                                      "\t"
                                                                      (format "%s" stars)
                                                                      " "
                                                                      consult-gh-star-icon
                                                                      "\n"
                                                                      (propertize (concat "last updated: " updated) 'face 'consult-gh-date))))
                                                  (unless (file-exists-p image-path)
                                                  (consult-gh-url-copy-file image-url image-path))
                                                (with-temp-file profile-path
                                                  (prin1 profile-text
                                                         (current-buffer)))
                                                nil)))))
    (let* ((image (create-image image-path nil nil :height (floor (* (frame-width) 1)))))

      (concat (if (and (file-exists-p image-path) (display-images-p))
                  (concat (propertize " " 'display image) "\n") "")
              (if (file-exists-p profile-path) (with-temp-buffer (insert-file-contents profile-path)
                                                                 (goto-char (point-min))
                                                                 (read (current-buffer)))
                repo)))))

(defun consult-gh--get-label-tooltip (label description color &rest _args)
  "Get tooltip for LABEL.

DESCRIPTION and COLOR are description and color of label from GitHub API."
  (concat (propertize label 'face `(t :background ,(concat "#" color) :box (:color ,(concat "#" color) :line-width (-1 . -2))))
          (if (stringp description)
                  (concat "\n"  (propertize description 'face 'consult-gh-description)))))

;;; Backend functions for `consult-gh'.

;; Buffers and Windows
(defun consult-gh-quit-window (&optional kill window)
  "Quit WINDOW and bury its buffer or delete WINDOW.

WINDOW must be a live window and defaults to the selected one.
This calls `quit-window' when there are more than one windows
and `delete-window' when there is only one window.

When KILL is non-nil, it kills the current buffer as well."
  (if (one-window-p)
      (quit-window kill window)
    (progn
      (when kill (kill-buffer (current-buffer)))
      (delete-window window))))

(defun consult-gh--completion-get-issue-list (string)
  "Filter function to parse STRING, json output of “gh issue list”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (mapcar (lambda (item) (cons (format "#%s" (gethash :number item)) (gethash :title item))) (consult-gh--json-to-hashtable string)))

(defun consult-gh--completion-set-issues (&optional topic repo)
  "Make async process to get list of issues of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic)))
         (issueEnabled (consult-gh--repo-has-issues-enabled-p repo)))
    (if (eq issueEnabled 't)
        (consult-gh--make-process "consult-gh-issue-list"
                                  :when-done (lambda (_ out)
                                     (add-text-properties 0 1 (list :issues (consult-gh--completion-get-issue-list out)) topic))
                                  :cmd-args (list "issue" "list" "--repo" repo "--state" "all" "--limit" consult-gh-completion-max-items "--search" "sort:updated" "--json" "number,title")))))

(defun consult-gh--completion-set-prs (&optional topic repo)
  "Make async process to get list of pull requests of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (consult-gh--make-process "consult-gh-pr-list"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list :prs (consult-gh--completion-get-issue-list out)) topic))
                              :cmd-args (list "pr" "list" "--repo" repo "--state" "all" "--search" "sort:updated" "--limit" consult-gh-completion-max-items "--json" "number,title"))))

(defun consult-gh--completion-get-mentionable-users-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (mapcar (lambda (item)
            (let* ((login (and (hash-table-p item) (gethash :login item))))
              (and (stringp login) (propertize (concat "@" login) 'help-echo (apply-partially #'consult-gh--get-user-tooltip login) 'rear-nonsticky t))))
          (consult-gh--json-to-hashtable string :mentionableUsers)))

(defun consult-gh--completion-set-mentionable-users (&optional topic repo)
  "Make async process to get list of mentionable users of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (consult-gh--make-process "consult-gh-mentionable-users"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list :mentionable-users (consult-gh--completion-get-mentionable-users-list out))
                                                      topic))
                              :cmd-args (list "repo" "view" repo "--json" "mentionableUsers"))))

(defun consult-gh--completion-get-assignable-users-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (mapcar (lambda (item) (let* ((login (and (hash-table-p item) (gethash :login item))))
                           (and (stringp login)
                             (propertize login 'help-echo (apply-partially #'consult-gh--get-user-tooltip login) 'rear-nonsticky t))))
(consult-gh--json-to-hashtable string :assignableUsers)))

(defun consult-gh--completion-set-assignable-users (&optional topic repo)
  "Make async process to get list of assignable users of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (consult-gh--make-process "consult-gh-valid-assignees"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :assignable-users (consult-gh--completion-get-assignable-users-list out))
                                                      topic))
                              :cmd-args (list "repo" "view" repo "--json" "assignableUsers"))))

(defun consult-gh--completion-get-labels-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (mapcar (lambda (item)
                                                              (when (hash-table-p item)
                                                                (let* ((name (gethash :name item))
                                                                       (desc (gethash :description item))
                                                                       (color (gethash :color item)))
                                                                  (when (stringp name)
                                                                    (propertize name 'help-echo (apply-partially #'consult-gh--get-label-tooltip name desc color) 'rear-nonsticky t)))))
          (consult-gh--json-to-hashtable string :labels)))

(defun consult-gh--completion-set-valid-labels (&optional topic repo)
  "Make async process to get list of labels of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (consult-gh--make-process "consult-gh-valid-labels"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :valid-labels (consult-gh--completion-get-labels-list out))
                                                      topic))
                              :cmd-args (list "repo" "view" repo "--json" "labels"))))

(defun consult-gh--completion-get-milestones-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (mapcar (lambda (item)
            (when (hash-table-p item)
              (let* ((title (gethash :title item))
                     (desc (gethash :description item)))
                (propertize title 'help-echo (concat (format "%s\n%s"
                                                             (or  title "")
                                                             (or  desc "no description")))
                            'rear-nonsticky t))))
          (consult-gh--json-to-hashtable string :milestones)))

(defun consult-gh--completion-set-valid-milestones (&optional topic repo)
  "Make async process to get list of milestones of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (consult-gh--make-process "consult-gh-valid-milestones"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :valid-milestones (consult-gh--completion-get-milestones-list out))
                                                      topic))
                              :cmd-args (list "repo" "view" repo "--json" "milestones"))))

(defun consult-gh--completion-get-projects-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (when (eq (consult-gh--json-to-hashtable string :hasProjectsEnabled) 't)
  (mapcar (lambda (item) (and (hash-table-p item) (gethash :title item))) (gethash :Nodes (consult-gh--json-to-hashtable string :projectsV2)))))

(defun consult-gh--completion-set-valid-projects (&optional topic repo)
  "Make async process to get list of milestones of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."

  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic)))
         (token-scopes (consult-gh--auth-get-token-scopes)))
    (when (or (member "read:project" token-scopes)
              (member "project" token-scopes))
        (consult-gh--make-process "consult-gh-valid-projects"
                                  :when-done (lambda (_ out)
                                     (add-text-properties 0 1 (list
                                                               :valid-projects (ignore-errors (consult-gh--completion-get-projects-list out)))
                                                          topic))
                                  :cmd-args (list "repo" "view" repo "--json" "hasProjectsEnabled,projectsV2")))))

(defun consult-gh--completion-get-branches-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (let ((branches (consult-gh--json-to-hashtable string)))
    (when (listp branches)
      (cl-loop for branch in branches
               collect
               (when (hash-table-p branch)
                 (propertize (gethash :name branch) :sha (gethash :sha (gethash :commit branch))))))))

(defun consult-gh--completion-set-branches (&optional topic repo)
  "Make async process to get list of branches of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (consult-gh--make-process "consult-gh-valid-branches"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :valid-refs (consult-gh--completion-get-branches-list out))
                                                      topic))
                              :cmd-args (list "api" (format "/repos/%s/branches" repo)))))

(defun consult-gh--completion-get-pr-refs-list (string repo refonly)
  "Filter function to parse STRING and get branches of REPO.

STRING is the json output of “gh api repos/repo/branches”.
When optional argument REFONLY is non-nil returns a list of branch naes only,
otherwise returns “OWNER:BRANCH”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (let ((branches (consult-gh--json-to-hashtable string)))
    (when (listp branches)
      (cl-loop for branch in branches
               collect
               (when (hash-table-p branch) (if refonly (gethash :name branch)
                                             (concat repo ":" (gethash :name branch))))))))

(defun consult-gh--completion-set-pr-refs (&optional topic baserepo headrepo refonly)
  "Make async process add branches of BASEREPO and HEADREPO in TOPIC.

When optional argument REFONLY is non-nil returns a list of branch naes only,
otherwise returns “OWNER:BRANCH”.
When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (baserepo (or baserepo (get-text-property 0 :baserepo topic)))
         (headrepo (or headrepo (get-text-property 0 :headrepo topic))))
    (when (stringp baserepo)
    (consult-gh--make-process "consult-gh-valid-basebranches"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :valid-baserefs (consult-gh--completion-get-pr-refs-list out (substring-no-properties baserepo) refonly))
                                                      topic))
                              :cmd-args (list "api" (format "/repos/%s/branches" baserepo))))
    (when (stringp headrepo)
    (consult-gh--make-process "consult-gh-valid-headbranches"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :valid-headrefs (consult-gh--completion-get-pr-refs-list out (substring-no-properties headrepo) refonly))
                                                      topic))
                              :cmd-args (list "api" (format "/repos/%s/branches" headrepo))))))

(defun consult-gh--completion-get-release-tags-list (string)
  "Filter function to parse STRING and get release tags.

STRING is the json output of “gh api repos/repo/tags”.
When optional argument REFONLY is non-nil returns a list of branch names only,
otherwise returns “OWNER:BRANCH”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
  (let ((tags (consult-gh--json-to-hashtable string)))
    (when (listp tags)
      (cl-loop for tag in tags
               collect
               (propertize (gethash :name tag) :sha (gethash :sha (gethash :commit tag)))))))

(defun consult-gh--completion-set-release-tags (&optional topic repo)
  "Make async process to add release tags of REPO in TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic))
         (repo (or repo (get-text-property 0 :repo topic))))
    (when (stringp repo)
    (consult-gh--make-process "consult-gh-valid-release-tags"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :valid-release-tags (consult-gh--completion-get-release-tags-list out))
                                                      topic))
                              :cmd-args (list "api" (format "/repos/%s/tags" repo))))))

(defun consult-gh--completion-get-repo-topics-list (string)
  "Filter function to parse STRING, json output of “gh view repo”.

This is a filter function suitable for passing to
`consult-gh--make-process'."
(consult-gh--json-to-hashtable string :name))

(defun consult-gh--completion-set-repo-topics (&optional topic)
  "Make async process to get list of popular topics for TOPIC.

When TOPIC is nil, uses buffer-local variable `consult-gh--topic'."
  (let* ((topic (or topic consult-gh--topic)))
    (consult-gh--make-process "consult-gh-valid-assignees"
                              :when-done (lambda (_ out)
                                 (add-text-properties 0 1 (list
                                                           :popular-topics (consult-gh--completion-get-repo-topics-list out))
                                                      topic))

                              :cmd-args (list "api" "repos/github/explore/contents/topics"))))

(defun consult-gh--completion-set-all-fields (&optional repo topic admin)
  "Make async process to get list of all fields of REPO in TOPIC.

ADMIN, is a boolean, whether the current user has permission to write
to REPO or not.

TOPIC is a string with properties containing metadata and defalts to
the buffer-local variable `consult-gh--topic'."

  (let*  ((type (get-text-property 0 :type topic)))
    ;; collect issues of repo for completion at point
    (consult-gh--completion-set-issues topic repo)
    ;; collect prs of repo for completion at point
    (consult-gh--completion-set-prs topic repo)
    ;; collect mentionable users for completion at point
    (consult-gh--completion-set-mentionable-users topic repo)
    ;; collect branches of the repo
    (consult-gh--completion-set-branches topic repo)
    ;; collect valid refs for completion at point
    (when (equal type "pr")
      (consult-gh--completion-set-pr-refs topic nil nil nil))

    (when (equal type "release")
      (consult-gh--completion-set-release-tags topic repo))

    (cond
     (admin
      ;; collect labels for completion at point
      (consult-gh--completion-set-valid-labels topic repo)
      ;; collect valid assignees for completion at point
      (consult-gh--completion-set-assignable-users topic repo)
      ;; collect valid milestones for completion at point
      (consult-gh--completion-set-valid-milestones topic repo)
      ;; collect valid projects for completion at point
      (consult-gh--completion-set-valid-projects topic repo))
     (t
      (add-text-properties 0 1 (list :valid-labels nil :assignable-users nil :valid-milestones nil :valid-projects nil) topic)))))

(defun consult-gh--topics-users-capf ()
  "Completion at point for users.

Completes “@.*” for mentionng users in comments, posts,..."
  (let* ((topic consult-gh--topic)
         (begin (match-beginning 0))
         (end (point))
         (candidates (completion-table-dynamic
                      (lambda (_)
                        (cl-remove-duplicates
                         (delq nil (append
                                    (get-text-property 0 :mentionable-users topic)
                                    (get-text-property 0 :commenters topic)))))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-user-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                      (when-let* ((cand (car (member str (cl-remove-duplicates
                         (delq nil (append
                                    (get-text-property 0 :mentionable-users topic)
                                    (get-text-property 0 :commenters topic))))))))
                      (and (stringp cand)
                           (add-text-properties (- (point) (length str)) (point)
                                          (text-properties-at 0 cand)))
                      (display-local-help)))))
    (list begin end candidates
          :affixation-function affix-fun
          :exit-function exit-fun
          :exclusive 'no
          :category 'string)))

(defun consult-gh--topics-issue-number-capf ()
  "Completion at point for issue numbers.

Completes “#.*” for referencing issues"
  (let* ((topic consult-gh--topic)
         (begin (match-beginning 0))
         (end (point))
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates
                          (delq nil
                                (mapcar (lambda (item)
                                          (when (consp item)
                                            (concat (car item)
                                                    "\t"
                                                    (propertize (cdr item) 'face 'completions-annotations))))
                                        (append (get-text-property 0 :issues topic)
         (get-text-property 0 :prs topic))))))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item
                                      (cond ((assoc (car (split-string item "\t" t))
                                                   (get-text-property 0 :issues topic))
                                             consult-gh-issue-icon)
                                            ((assoc (car (split-string item "\t" t))
                                                    (get-text-property 0 :prs topic))
                                             consult-gh-pr-icon)
                                            (t ""))
                                      ""))
                              list)))
         (exit-fun (lambda (str _status)
              (delete-char (- (length str)))
              (when (looking-back "#" (- (point) 1)) (delete-char -1))
              (let* ((b (point))
                     (number (car (split-string str "\t" t)))
                     (icon (cond
                            ((assoc number (get-text-property 0 :issues topic))
                               consult-gh-issue-icon)
                            ((assoc number (get-text-property 0 :prs topic))
                             consult-gh-pr-icon))))


                (insert (or number str))
                (add-text-properties b (point)
                                     (list 'help-echo (concat icon "\s" str) 'rear-nonsticky t))
                (display-local-help)))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'no
      :category 'string)))

(defun consult-gh--topics-issue-title-capf ()
  "Completion at point for issue title.

Completes “#.*” for referencing issues"
  (let* ((topic consult-gh--topic)
         (begin (1+ (match-beginning 0)))
         (end (point))
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates
                          (delq nil (mapcar #'cdr (append (get-text-property 0 :issues topic)
                                                          (get-text-property 0 :prs topic))))))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item
                                      (cond ((rassoc item (get-text-property 0 :issues topic))
                                             (concat  consult-gh-issue-icon " " (propertize (or (car-safe (rassoc item (get-text-property 0 :issues topic))) "") 'face 'completions-annotations) " "))
                                            ((rassoc item (get-text-property 0 :prs topic))
                                             (concat consult-gh-pr-icon " " (propertize (or (car-safe (rassoc item (get-text-property 0 :prs topic))) "") 'face 'completions-annotations) " "))
                                            (t ""))
                                      ""))
                              list)))
         (exit-fun (lambda (str _status)
              (delete-char (- (length str)))
              (when (looking-back "#" (- (point) 1)) (delete-char -1))
              (let* ((b (point))
                    (issue (rassoc str (get-text-property 0 :issues topic)))
                    (pr (rassoc str (get-text-property 0 :prs topic)))
                    (icon (cond
                           (issue consult-gh-issue-icon)
                           (pr consult-gh-pr-icon)))
                    (number (cond
                             (issue (car-safe issue))
                             (pr (car-safe pr))))
                    (title (cond
                             (issue (cdr-safe issue))
                             (pr (cdr-safe pr)))))
              (insert (or number str))
              (when (and title number (stringp title) (stringp number))
                (add-text-properties b (point)
                                     (list 'help-echo (concat icon "\s" number "\t" title) 'rear-nonsticky t))
                (display-local-help))))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'no
      :category 'string)))

(defun consult-gh--topics-baseref-branch-capf ()
  "Completion at point for base reference branches.

Completes “base:.*” for referencing branches"
  (let* ((topic consult-gh--topic)
         (baserefs (completion-table-dynamic
                      (lambda (_)
                         (get-text-property 0 :valid-baserefs topic))))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}base: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-branch-icon ""))
                              list))))
(list begin end baserefs
      :affixation-function affix-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-headref-branch-capf ()
  "Completion at point for head reference branches.

Completes “head:.*” for referencing branches"
  (let* ((topic consult-gh--topic)
         (headrefs (completion-table-dynamic
                      (lambda (_)
                         (get-text-property 0 :valid-headrefs topic))))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}head: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-branch-icon ""))
                              list))))
(list begin end headrefs
      :affixation-function affix-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-default-branch-capf ()
  "Completion at point for default branch.

Completes “default_branch:.*” for referencing branches"
  (let* ((topic consult-gh--topic)
         (headrefs (completion-table-dynamic
                      (lambda (_)
                         (get-text-property 0 :valid-refs topic))))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}default_branch: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-branch-icon ""))
                              list))))
(list begin end headrefs
      :affixation-function affix-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-assignees-capf ()
  "Completion at point for assignees.

Completes “assignees:.*” for adding assignees."
  (let* ((topic consult-gh--topic)
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates (delq nil (append (list "@copilot") (get-text-property 0 :assignable-users topic)))))))
        (begin (save-excursion
                  (cond
                   ((looking-back ", " (- (point) 2))
                          (point))
                   ((looking-back "^.\\{1,3\\}assignees: " (pos-bol))
                    (point))
                   ((looking-back "," (- (point) 1))
                    (point))
                   ((re-search-backward ", " (pos-bol) t)
                    (+ (point) 2))
                   (t
                    (backward-word)
                    (point)))))
         (end (save-excursion
                (cond
                 ((looking-at "\\(?1:.*?\\),")
                  (max (or (match-end 1) (point)) begin))
                 (t
                  (point)))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-user-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                      (when-let* ((cand (car (member str (delq nil (get-text-property 0 :assignable-users topic))))))
                        (and (stringp cand)
                             (add-text-properties (- (point) (length str)) (point)
                                          (text-properties-at 0 cand))
                             (display-local-help)))
                     (save-excursion
                       (backward-char (length str))
                       (when (looking-back "," (- (point) 1)) (insert " ")))
                     (insert ", "))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-labels-capf ()
  "Completion at point for labels.

Completes “labels:.*” for adding labels."
  (let* ((topic consult-gh--topic)
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates (delq nil (get-text-property 0 :valid-labels topic))))))
         (begin (save-excursion
                  (cond
                   ((looking-back ", " (- (point) 2))
                          (point))
                   ((looking-back "," (- (point) 1))
                    (point))
                   ((re-search-backward ", " (pos-bol) t)
                    (+ (point) 2))
                   ((looking-back "^.\\{1,3\\}labels: " (pos-bol))
                    (point))
                   ((looking-back "^.\\{1,3\\}labels: \\(?1:[^,]+?\\)" (pos-bol))
                    (or (match-beginning 1) (progn
                                              (backward-word)
                                              (point))))
                   (t
                    (backward-word)
                    (point)))))
         (end (save-excursion
                (cond
                 ((looking-at "\\(?1:.*?\\),")
                  (max (or (match-end 1) (point)) begin))
                 (t
                  (point)))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-label-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                      (when-let* ((cand (car (member str (delq nil (get-text-property 0 :valid-labels topic))))))
                        (and (stringp cand)
                             (add-text-properties (- (point) (length str)) (point)
                                          (text-properties-at 0 cand))
                             (display-local-help)))
                     (save-excursion
                       (backward-char (length str))
                       (when (looking-back "," (- (point) 1)) (insert " ")))
                     (insert ", "))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-milestone-capf ()
  "Completion at point for milestone.

Completes “milestone:.*” for adding a milestone."
  (let* ((topic consult-gh--topic)
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates (delq nil (get-text-property 0 :valid-milestones topic))))))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}milestone: " (pos-bol) t)
                        (point))
                       (t
                          (backward-word)
                          (point))))))
         (end (pos-eol))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-milestone-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                      (when-let* ((cand (car (member str (delq nil (get-text-property 0 :valid-milestones topic))))))
                        (and (stringp cand)
                             (add-text-properties (- (point) (length str)) (point)
                                          (text-properties-at 0 cand))
                             (display-local-help))))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-projects-capf ()
  "Completion at point for projects.

Completes “projects:.*” for adding projects."
  (let* ((topic consult-gh--topic)
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates (delq nil (get-text-property 0 :valid-projects topic))))))
         (begin (save-excursion
                  (cond
                   ((looking-back ", " (- (point) 2))
                          (point))
                   ((looking-back "," (- (point) 1))
                    (point))
                   ((re-search-backward ", " (pos-bol) t)
                    (+ (point) 2))
                   ((looking-back "^.\\{1,3\\}projects: " (pos-bol))
                    (point))
                   ((looking-back "^.\\{1,3\\}projects: \\(?1:[^,]+?\\)" (pos-bol))
                    (or (match-beginning 1) (progn
                                              (backward-word)
                                              (point))))
                   (t
                    (backward-word)
                    (point)))))
         (end (save-excursion
                (cond
                 ((looking-at "\\(?1:.*?\\),")
                  (max (or (match-end 1) (point)) begin))
                 (t
                  (point)))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-project-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                     (save-match-data
                       (when (string-match ".*,.*" str)
                         (delete-char (- (length str)))
                         (insert "\""
                                 str
                                 "\"")))
                     (save-excursion
                       (backward-char (length str))
                       (when (looking-back "," (- (point) 1)) (insert " ")))
                     (insert ", "))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-reviewers-capf ()
  "Completion at point for reviewers.

Completes “reviewers:.*” for adding reviewers."
  (let* ((topic consult-gh--topic)
         (author (get-text-property 0 :author topic))
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates (delq nil (remove author (get-text-property 0 :assignable-users topic)))))))
         (begin (if (looking-back " " (- (point) 1))
                          (point)
                        (save-excursion
                          (backward-word)
                          (point))))
         (end (if (looking-at "\\(?1:[^[:space:]]+?\\)," (pos-eol))
                   (save-excursion
                          (forward-word)
                          (point))
                (point)))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-user-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                      (when-let* ((cand (car (member str (delq nil (get-text-property 0 :assignable-users topic))))))
                        (and (stringp cand)
                             (add-text-properties (- (point) (length str)) (point)
                                          (text-properties-at 0 cand))
                             (display-local-help)))
                      (save-excursion
                       (backward-char (length str))
                       (when (looking-back "," (- (point) 1)) (insert " ")))
                     (insert ", "))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-release-tags-capf ()
  "Completion at point for reference release tags.

Completes “target:.*” for referencing tag names in a release"
  (let* ((topic consult-gh--topic)
         (tags (completion-table-dynamic
                      (lambda (_)
                         (get-text-property 0 :valid-release-tags topic))))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}tag: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-tag-icon ""))
                              list))))
(list begin end tags
      :affixation-function affix-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-branch-capf ()
  "Completion at point for reference branches.

Completes “target:.*” for referencing branches in a release"
  (let* ((topic consult-gh--topic)
         (targets (completion-table-dynamic
                      (lambda (_)
                         (get-text-property 0 :valid-refs topic))))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}target: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-branch-icon ""))
                              list))))
(list begin end targets
      :affixation-function affix-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-prerelease-capf ()
  "Completion at point for release pre-release.

Completes “prerelease:.*” for selecting pre-release in a release."
  (let* ((targets '("true" "false"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}prerelease: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-draft-capf ()
  "Completion at point for release draft.

Completes “prerelease:.*” for selecting draft in a release."
  (let* ((targets '("true" "false"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}draft: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-topics-capf ()
  "Completion at point for repo topics.

Completes “topics:.*” for adding topics."
  (let* ((topic consult-gh--topic)
         (candidates (completion-table-dynamic
                      (lambda (_)
                         (cl-remove-duplicates (delq nil (get-text-property 0 :popular-topics topic))))))
        (begin (save-excursion
                  (cond
                   ((looking-back ", " (- (point) 2))
                          (point))
                   ((looking-back "^.\\{1,3\\}topics: " (pos-bol))
                    (point))
                   ((looking-back "," (- (point) 1))
                    (point))
                   ((re-search-backward ", " (pos-bol) t)
                    (+ (point) 2))
                   (t
                    (backward-word)
                    (point)))))
         (end (save-excursion
                (cond
                 ((looking-at "\\(?1:.*?\\),")
                  (max (or (match-end 1) (point)) begin))
                 (t
                  (point)))))
         (affix-fun (lambda (list)
                      (mapcar (lambda (item)
                                (list item consult-gh-topic-icon ""))
                              list)))
         (exit-fun (lambda (str _status)
                      (when-let* ((cand (car (member str (delq nil (get-text-property 0 :popular-topics topic))))))
                        (and (stringp cand)
                             (add-text-properties (- (point) (length str)) (point)
                                          (text-properties-at 0 cand))
                             (display-local-help)))
                     (save-excursion
                       (backward-char (length str))
                       (when (looking-back "," (- (point) 1)) (insert " ")))
                     (insert ", "))))
(list begin end candidates
      :affixation-function affix-fun
      :exit-function exit-fun
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-template-capf ()
  "Completion at point for repo template.

Completes “template:.*” for selecting template in a repo edit buffer."
  (let* ((targets '("true" "false"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}template: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-issues-capf ()
  "Completion at point for enabling repo issues.

Completes “issues:.*” for enabling issues in a repo edit buffer."
  (let* ((targets '("enabled" "disabled"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}issues: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-projects-capf ()
  "Completion at point for enabling repo projects.

Completes “projects:.*” for enabling projects in a repo edit buffer."
  (let* ((targets '("enabled" "disabled"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}projects: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-discussions-capf ()
  "Completion at point for enabling repo discussions.

Completes “discussions:.*” for enabling discussions in a repo edit buffer."
  (let* ((targets '("enabled" "disabled"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}discussions: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-visibility-capf ()
  "Completion at point for changing visibility.

Completes “visibility:.*” for changing visibility in a repo edit buffer."
  (let* ((targets '("PRIVATE" "PUBLIC" "INTERNAL"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}visibility: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-wiki-capf ()
  "Completion at point for enabling repo wiki.

Completes “wiki:.*” for enabling wiki in a repo edit buffer."
  (let* ((targets '("enabled" "disabled"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}wiki: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-merge-commit-capf ()
  "Completion at point for enabling merge commit in repo.

Completes “merge_commit:.*” for enabling merge commit in a repo edit
buffer."
  (let* ((targets '("allowed" "not allowed"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}merge_commit: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-squash-merge-capf ()
  "Completion at point for enabling squash merge in repo.

Completes “squash_merge:.*” for enabling squash merge in a repo edit
buffer."
  (let* ((targets '("allowed" "not allowed"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}squash_merge: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-rebase-merge-capf ()
  "Completion at point for enabling rebase merge in repo.

Completes “rebase_merge:.*” for enabling rebase merge in a repo edit
buffer."
  (let* ((targets '("allowed" "not allowed"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}rebase_merge: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-repo-enable-delete-on-merge-capf ()
  "Completion at point for enabling delete on merge in repo.

Completes “delete_on_merge:.*” for enabling delete on merge in a repo
edit buffer."
  (let* ((targets '("yes" "no"))
         (begin (or (match-beginning 1)
                    (save-excursion
                      (cond
                       ((re-search-backward "^.\\{1,3\\}delete_on_merge: " (pos-bol) t)
                        (point))
                       ((looking-back " " (- (point) 1))
                        (point))
                       (t
                        (backward-word)
                        (point))))))
         (end (pos-eol)))
(list begin end targets
      :exclusive 'yes
      :category 'string)))

(defun consult-gh--topics-edit-capf ()
  "Completion at point for editing comments.

Completes for issue/pr numbers or user names."
  (save-match-data
    (when consult-gh-topics-edit-mode
      (cond
       ((looking-back "@[^[:space:]]*?" (pos-bol))
        (consult-gh--topics-users-capf))
       ((looking-back "#[^#\\+[:space:][:digit:]]+?" (pos-bol))
         (consult-gh--topics-issue-title-capf))
       ((looking-back "#[^#\\+[:space:]]*?" (pos-bol))
        (consult-gh--topics-issue-number-capf))
        ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}base: \\(?1:.*\\)" (pos-bol)))
         (consult-gh--topics-baseref-branch-capf))
         ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}head: \\(?1:.*\\)" (pos-bol)))
          (consult-gh--topics-headref-branch-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}assignees: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-assignees-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}labels: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-labels-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}milestone: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-milestone-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}projects: \\(?1:.*\\)" (pos-bol)))
        (if (equal (get-text-property 0 :type consult-gh--topic) "repo")
            (consult-gh--topics-repo-enable-projects-capf)
          (consult-gh--topics-projects-capf)))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}reviewers: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-reviewers-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}target: \\(?1:.*\\)" (pos-bol)))
         (consult-gh--topics-branch-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}draft: \\(?1:.*\\)" (pos-bol)))
         (consult-gh--topics-draft-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}prerelease: \\(?1:.*\\)" (pos-bol)))
         (consult-gh--topics-prerelease-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}tag: \\(?1:.*\\)" (pos-bol)))
         (consult-gh--topics-release-tags-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}default_branch: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-default-branch-capf))
        ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}visibility: \\(?1:.*\\)" (pos-bol)))
         (consult-gh--topics-repo-visibility-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}topics: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-topics-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}template: \\(?1:.*\\)" (pos-bol)))
       (consult-gh--topics-repo-template-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}issues: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-issues-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}discussions: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-discussions-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}wiki: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-wiki-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}merge_commit: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-merge-commit-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}squash_merge: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-squash-merge-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}rebase_merge: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-rebase-merge-capf))
       ((and (get-text-property (pos-bol) 'read-only) (looking-back "^.\\{1,3\\}delete_on_merge: \\(?1:.*\\)" (pos-bol)))
        (consult-gh--topics-repo-enable-delete-on-merge-capf))))))

(defun consult-gh--auth-accounts ()
  "Return a list of currently autheticated accounts.

Each account is in the form \='(USERNAME HOST IF-ACTIVE)."
  (let* ((str (consult-gh--command-to-string "auth" "status"))
         (i 0)
         (accounts nil))
    (while (and (stringp str) (string-match "Logged in to \\(.+\\)? account \\(.+\\)? \(.*\)\n.*Active account: \\(.+\\)?" str i)
                (> (match-end 0) i))
      (let ((m (match-data))
            (host (match-string 1 str))
            (name (match-string 2 str))
            (active (equal (match-string 3 str) "true")))
        (push `(,name ,host ,active) accounts)
        (setq i (cadr m))))
    accounts))

(defun consult-gh--auth-current-active-account (&optional host)
  "Return currently logged-in active account.

This is a list of \='(USERNAME HOST IF-ACTIVE)."
  (let* ((accounts (consult-gh--auth-accounts))
         (host (or host consult-gh-default-host))
         (current-account (car-safe (seq-filter (lambda (acc) (and (equal (cadr acc) host) (caddr acc) acc)) accounts))))
    (when current-account
      (setq consult-gh--auth-current-account current-account))))

(defvar consult-gh-auth-post-switch-hook nil
  "Functions called after `consult-auth--switch'.

host and username are passed to these functions.")

(defun consult-gh--auth-switch (host user)
"Authentication the account for USER on HOST.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-auth-switch'."
(if (and (stringp host) (stringp user))
  (let ((str (consult-gh--command-to-string "auth" "switch" "-h" host "-u" user)))
    (when (stringp str)
      (setq consult-gh--auth-current-account `(,user ,host t))
      (run-hook-with-args 'consult-gh-auth-post-switch-hook user host)
      (message str)))
  (message "%s" (concat (propertize "HOST" 'face 'warning) "and " (propertize "USER" 'face 'warning) "need to be provided as strings."))))

(defun consult-gh--auth-get-token-scopes (&optional username host)
  "Return a list of token scopes for USERNAME on HOST.

USERNAME and HOST default to `consult-gh--auth-current-account'."
  (let* ((username (or username (car-safe consult-gh--auth-current-account) (car-safe  (consult-gh--auth-current-active-account)) ".*?"))
         (host (or host (and (consp consult-gh--auth-current-account) (cadr consult-gh--auth-current-account)) (cadr (consult-gh--auth-current-active-account)) ".*?"))
         (str (consult-gh--command-to-string "auth" "status")))
    (when
        (string-match (format "Logged in to %s account %s \(.*\)\n.*Active account: true[[:ascii:][:nonascii:]]*?Token scopes: \\(?1:.+\\)?" host username) str)
      (let ((m (match-string 1 str)))
        (and (stringp m) (split-string m ", " t "['\s\n\t]"))))))

(defun consult-gh--set-current-user-orgs (&rest _args)
  "Return list of the orgs for the current user."
  (or consult-gh--current-user-orgs
      (setq consult-gh--current-user-orgs (consult-gh--get-current-user-orgs nil t))))

(defun consult-gh--update-current-user-orgs (&rest _args)
  "Update the list of orgs for the current user.

Sets `consult-gh--current-user-orgs' for the current user."
  (setq consult-gh--current-user-orgs (consult-gh--get-current-user-orgs nil t)))

;; add hook to set user orgs after switching accounts
(add-hook 'consult-gh-auth-post-switch-hook #'consult-gh--update-current-user-orgs)

(defun consult-gh-topics--get-buffer-create (name subject topic)
  "Get or create a buffer with NAME for SUBJECT and TOPIC.

Description of Arguments:
  NAME    a strig; name of the buffer
  SUBJECT a string; the subject of the content
          (e.g. repo, comment, issue, pull request, etc.)
  TOPIC   a string; string with properties that identify the topic
          (see `consult-gh--topic' for example)"
  (let* ((buffer (get-buffer-create name))
         (existing (not (= (buffer-size buffer) 0)))
         (confirm (if existing
                      (consult--read
                       (list (cons "Resume editing/viewing in the existing buffer." :resume)
                             (cons (format "Create a new buffer for %s from scratch, but do not discard the old one." subject) :new)
                             (cons "Discard the old draft/buffer and create a new one from scratch." :replace))
                       :prompt (format "You already have an existing buffer for this %s.  Would you like to resume editing/viewing that one or start a new one? " subject)
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))

    (when existing
      (cond
       ((eq confirm :resume) (setq existing t))
       ((eq confirm :replace) (setq existing nil))
       ((eq confirm :new)
        (setq existing nil)
        (setq buffer (generate-new-buffer name nil)))))

    (with-current-buffer buffer
      (unless existing
        (let ((inhibit-read-only t))
          (erase-buffer)
          (pcase consult-gh-topic-major-mode
            ('gfm-mode (gfm-mode) (markdown-display-inline-images))
            ('markdown-mode (markdown-mode) (markdown-display-inline-images))
            ('org-mode (org-mode))
            (_ (outline-mode)))))
      (setq-local consult-gh--topic topic)
      (consult-gh-topics-edit-mode +1)
      (goto-char (point-max))
      (with-no-warnings (outline-show-all))
      (current-buffer))))

(defun consult-gh-topics--buffer-string (&optional buffer)
  "Get BUFFER string for consult-gh-topics.

Extracts the buffer string after removing consult-gh specific regions.
This inclused regions with the text property \=:consult-gh-comments or
regions with an overlay of \=:consult-gh-header."
  (let* ((text (consult-gh--whole-buffer-string buffer))
         (header-regions (consult-gh--get-region-with-overlay ':consult-gh-header))
         (mode (cond
                ((derived-mode-p 'gfm-mode) 'gfm-mode)
                ((derived-mode-p 'markdown-mode) 'markdown-mode)
                ((derived-mode-p 'org-mode) 'org-mode)
                (t 'text-mode))))
    (with-temp-buffer
      (let ((inhibit-read-only t))
        (insert text)
        (when header-regions
          (cl-loop for region in header-regions
                   do (delete-region (car region) (cdr region))))
        (consult-gh--delete-region-with-prop :consult-gh-comments)
        (consult-gh--delete-region-with-prop :consult-gh-markings)
        (cond
         ((eq mode 'gfm-mode)
          (gfm-mode)
          (consult-gh--whole-buffer-string buffer))
         ((eq mode 'markdown-mode)
          (markdown-mode)
          (consult-gh--whole-buffer-string buffer))
         ((eq mode 'org-mode)
          (org-mode)
          (consult-gh--org-to-markdown buffer))
         (t (text-mode)
            (consult-gh--whole-buffer-string buffer)))))))

(defun consult-gh-topics--get-title-and-body (&optional buffer)
  "Parse the BUFFER to get title and body of comment.

BUFFER defaults to the `current-buffer'."
  (let* ((text (consult-gh--whole-buffer-string buffer))
         (header-regions (consult-gh--get-region-with-overlay ':consult-gh-header))
         (header-beg (car-safe (car-safe header-regions)))
         (header-end (cdr-safe (car-safe header-regions)))
         (mode (cond
                ((derived-mode-p 'gfm-mode) 'gfm-mode)
                ((derived-mode-p 'markdown-mode) 'markdown-mode)
                ((derived-mode-p 'org-mode) 'org-mode)
                (t 'text-mode))))
    (with-temp-buffer
      (let ((inhibit-read-only t)
            (title nil)
            (body nil))
        (insert text)
        (consult-gh--delete-region-with-prop :consult-gh-comments)
        (consult-gh--delete-region-with-prop :consult-gh-markings)
        (cond
         ((eq mode 'gfm-mode)
          (gfm-mode))
         ((eq mode 'markdown-mode)
          (markdown-mode))
         ((eq mode 'org-mode)
          (org-mode))
         (t (text-mode)))
        (goto-char (or header-beg (point-min)))
        (cond
         ((looking-at "\\`# title: *\\|\\`#\\+title: *")
          (goto-char (match-end 0))
          (setq title (string-trim
                       (buffer-substring (point) (line-end-position)))))
         (t
          (goto-char (point-min))
          (when (re-search-forward "^\\`# title: *\\|^\\`#\\+title: *" (if header-end header-end nil) t)
            (setq title (string-trim
                       (buffer-substring (point) (line-end-position)))))))
        (goto-char (point-min))
        (when header-regions
          (cl-loop for region in header-regions
                   do (delete-region (car region) (cdr region))))
        (setq body (string-trim
                    (if (eq mode 'org-mode)
                        (consult-gh--org-to-markdown)
                      (consult-gh--whole-buffer-string))))
(cons title body)))))

(defun consult-gh-topics--format-field-header-string (string &optional prefix suffix)
  "Make a read-only field header from STRING.

When optional arguments PREFIX, or SUFFIX are non-nil, add them
as normal text without propeties before or after STRING.

This is useful to create non-editable fields for forms such as
“Title: ” or “Date: ” in the line."
  (concat prefix
          (propertize (substring string 0 -1) 'read-only t 'cursor-intangible t)
          (propertize (substring string -1) 'read-only t 'cursor-intangible t 'rear-nonsticky t)
          suffix))

(defun consult-gh-topics--insert-field-header-string (string &optional prefix suffix)
  "Insert a read-only field header from STRING.

When optional arguments PREFIX, or SUFFIX are non-nil, add them
as normal text without propeties before or after STRING.

This inserts the string created with
`consult-gh-topics--format-field-header-string'"

(insert (consult-gh-topics--format-field-header-string string prefix suffix))
(when (derived-mode-p 'markdown-mode) (delete-char -1) (insert " ")))

(cl-defun consult-gh-topics--insert-repo-contents (buffer topic &rest args &key name owner defaultBranch body description visibility homepageUrl repoTopics isTemplate issuesEnabled discussionsEnabled wikiEnabled projectsEnabled squashMergeAllowed rebaseMergeAllowed mergeCommitAllowed deleteOnMerge &allow-other-keys)
  "Fill the BUFFER with TOPIC and ARGS.

Description of Arguments:
  TOPIC               a string; string with properties that identify the
                      topic (see `consult-gh--topic' for example)
  NAME                a string; name of repo
  OWNER               a string; owner of repo
  DEFAULTBRANCH       a string; name of the default branch
  BODY                a string; body of repo
  DESCRIPTION         a string; description of repo
  VISIBILITY          a string; visibility of repo
                      “private”, “public” or“internal”
  HOMEPAGEURL         a string; homepage url of repo
  REPOTOPICS          a list; list of relevant topics of repo
  ISTEMPLATE          a boolean; whether repo is a template
  ISSUESENABLED       a boolean; whether issues are enabled
  DISCUSSIONSENABLED  a boolean; whether discussions are enabled
  WIKIENABLED         a boolean; whether wiki is enabled
  PROJECTSENABLED     a boolean; whether projects are enabled
  SQUASHMERGEALLOWED  a boolean; whether squash merge is allowed
  REBASEMERGEALLOWED  a boolean; whether rebase merge is allowed
  MERGECOMMITALLOWED  a boolean; whether merge commit is allowed
  DELETEONMERGE       a boolean; whether to delete head branch on merge"

  (let* ((type "repo")
         (buff (consult-gh-topics--get-buffer-create buffer type topic)))
    (with-current-buffer buff
      (unless (not (= (buffer-size) 0))
        (pcase-let* ((inhibit-read-only t)
                     (`(,title-marker ,header-marker)
                      (consult-gh-topics--markers-for-metadata)))

          (consult-gh-topics--insert-field-header-string (concat title-marker "name: " name))
          (insert "\n")

          (consult-gh-topics--insert-field-header-string (concat header-marker "owner: " owner))
          (insert "\n")

          (save-mark-and-excursion
            (let* ((beg (point-min))
                   (end nil))

              (consult-gh-topics--insert-field-header-string (concat header-marker "default_branch: "))
                (when (stringp defaultBranch)
                (insert defaultBranch))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "description: "))
                (when (stringp description)
                (insert description))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "visibility: "))
                (when (stringp visibility)
                (insert visibility))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "homepage: "))
                (when (stringp homepageUrl)
                (insert homepageUrl))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "topics: "))
              (when repoTopics
                  (cond
                   ((stringp repoTopics)
                    (insert repoTopics))
                   ((and repoTopics (listp repoTopics))
                    (insert (consult-gh--list-to-string repoTopics)))))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "template: "))
                (if isTemplate (insert "true") (insert "false"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "issues: "))
                (if issuesEnabled (insert "enabled") (insert "disabled"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "projects: "))
                (if projectsEnabled (insert "enabled") (insert "disabled"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "discussions: "))
                (if discussionsEnabled (insert "enabled") (insert "disabled"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "wiki: "))
                (if wikiEnabled (insert "enabled") (insert "disabled"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "merge_commit: "))
                (if mergeCommitAllowed (insert "allowed") (insert "not allowed"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "squash_merge: "))
                (if squashMergeAllowed (insert "allowed") (insert "not allowed"))
                (insert "\n")

                 (consult-gh-topics--insert-field-header-string (concat header-marker "rebase_merge: "))
                (if rebaseMergeAllowed (insert "allowed") (insert "not allowed"))
                (insert "\n")

                (consult-gh-topics--insert-field-header-string (concat header-marker "delete_on_merge: "))
                (if deleteOnMerge (insert "yes") (insert "no"))
                (insert "\n")

            (insert (consult-gh-topics--format-field-header-string "-----\n"))

            (setq end (point))
            (overlay-put (make-overlay beg end) :consult-gh-header t)
            (insert "\n"))
          (when body (insert body
                             "\n"))
          (cursor-intangible-mode +1)))))))

(cl-defun consult-gh-topics--insert-buffer-contents (buffer topic &rest args &key title body assignees labels milestone projects reviewers baserepo basebranch headrepo headbranch target tagname draft prerelease canwrite &allow-other-keys)
  "Fill the BUFFER with TOPIC and ARGS.

Description of Arguments:
  TOPIC              a string; string with properties that identify the
                     topic (see `consult-gh--topic' for example)
  TITLE              a string; title of the topic
  BODY               a string; body of the topic
  ASSIGNEES          a list of strings; new list of assignees
  LABELS             a list of strings; new list of labels
  MILESTONE          a string; new milestone
  PROJECTS           a list of strings; new list of projects
  REVIEWERS          a list of strings; new list of reviewers
  BASEREPO           a string; name of the base (target) repository
  BASEBRANCH         a string; name of the base ref branch
  HEADREPO           a string; name of the head (source) repository
  HEADBRANCH         a string; name of the head ref branch
  TARGET             a string; target branch or commit for release
  TAGNAME            a string; tag name for a release
  DRAFT              a boolean; whether release is a draft
  PRERELEASE         a boolean; whether release is a prerelease
  CANWRITE           a boolean; whether the current user has write
                     permission on topic"

  (let* ((type (get-text-property 0 :type topic))
         (subject (pcase type
                    ("repo" "rpeo")
                    ("issue" "issue")
                    ("pr" "pull request")
                    ("release" "release")
                    ("commit message" "commit")
                    (_ type)))
         (buff (consult-gh-topics--get-buffer-create buffer subject topic)))
    (with-current-buffer buff
      (unless (not (= (buffer-size) 0))
        (pcase-let* ((inhibit-read-only t)
                     (`(,title-marker ,header-marker)
                      (consult-gh-topics--markers-for-metadata)))

             (consult-gh-topics--insert-field-header-string (concat title-marker "title: "))
             (when title (insert title))

             (save-mark-and-excursion
               (insert "\n")
               (let* ((beg (point-min))
                      (end nil))

                 (when (equal type "pr")
                   (when (or baserepo basebranch)
                     (consult-gh-topics--insert-field-header-string (concat header-marker "base: "))
                     (when (stringp baserepo)
                       (insert baserepo))
                     (when (stringp basebranch)
                       (insert (concat (if baserepo ":") basebranch)))
                     (insert "\n"))

                   (when (or headrepo headbranch)
                     (consult-gh-topics--insert-field-header-string (concat header-marker "head: "))
                     (when (stringp headrepo)
                       (insert headrepo))
                     (when (stringp headbranch)
                       (insert (concat (if headrepo ":") headbranch)))
                     (insert "\n"))

                   (when canwrite
                     (consult-gh-topics--insert-field-header-string (concat header-marker "reviewers: "))
                     (when reviewers
                       (cond
                        ((stringp reviewers)
                         (insert reviewers))
                        ((and reviewers (listp reviewers))
                         (insert (consult-gh--list-to-string reviewers)))))
                     (insert "\n")))

                 (when (and (or (equal type "pr")
                                (equal type "issue"))
                            canwrite)
                   (consult-gh-topics--insert-field-header-string (concat header-marker "assignees: "))
                   (when assignees
                     (cond
                      ((stringp assignees)
                       (insert assignees))
                      ((and assignees (listp assignees))
                       (insert (consult-gh--list-to-string assignees)))))
                   (insert "\n")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "labels: "))
                   (when labels
                     (cond
                      ((stringp labels)
                       (insert labels))
                      ((and labels (listp labels))
                       (insert (consult-gh--list-to-string labels)))))
                   (insert "\n")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "milestone: "))
                   (when (and milestone (stringp milestone))
                     (insert milestone))
                   (insert "\n")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "projects: "))
                   (when projects
                     (cond
                      ((stringp projects)
                       (insert projects))
                      ((and projects (listp projects))
                       (insert (consult-gh--list-to-string projects)))))
                   (insert "\n"))

                 (when (equal type "release")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "tag: "))
                   (when (stringp tagname)
                     (insert tagname))
                   (insert "\n")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "target: "))
                   (when (stringp target)
                     (insert target))
                   (insert "\n")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "draft: "))
                   (if (and draft (not (equal draft :false)))
                       (insert "true")
                     (insert "false"))
                   (insert "\n")

                   (consult-gh-topics--insert-field-header-string (concat header-marker "prerelease: "))
                   (if (and prerelease (not (equal prerelease :false)))
                       (insert "true")
                     (insert "false"))
                   (insert "\n"))

                 (insert (consult-gh-topics--format-field-header-string "-----\n"))

                 (setq end (point))
                 (overlay-put (make-overlay beg end) :consult-gh-header t)
                 (insert "\n"))
               (when body (insert body
                                  "\n"))
               (cursor-intangible-mode +1)))))))

(defun consult-gh-topics--create-add-metadata-header (header content replace &optional topic meta)
"Add CONTENT to HEADER of TOPIC.

When REPLACE is non-nil, replace the old content with CONTENT.

Description of Arguments:
 HEADER        a string; title of metadata header
 CONTENT       a string; new content for the header
 REPLACE       a boolean; whether to replace the

               content of header.  When nil, the
               content is appended to the current/old
               content
 TOPIC         a string; string with properties that
               identify the topic (see
               `consult-gh--topic' for example)
 META          an alist; the current/old metadata"
(let* ((keyword-str (unless (string-prefix-p ":" header)
                  (concat ":" header)))
       (keyword (intern keyword-str))
       (topic (or topic consult-gh--topic))
       (meta (or meta (consult-gh-topics--issue-get-metadata topic)))
       (header-region (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
       (current (cdr (assoc header meta)))
       (content (cond
                 ((not current) content)
                 ((and (listp current) (listp content) content))
                 ((and (listp current) (stringp content))
                  (list content))
                 ((and (stringp current) (stringp content))
                  (if (string-empty-p content)
                      nil
                    content))
                 ((and (stringp current) (listp content))
                  (consult-gh--list-to-string content))
                 (t content)))
       (new-content (if replace
                        content
                      (cond
                       ((listp content)
                      (cl-remove-duplicates (delq nil (append current content))
                                            :test #'equal))
                       ((stringp content)

                        (if (and current (stringp current))
                            (concat current ", " content)
                          content))))))

  (add-text-properties 0 1 (list keyword new-content) topic)

             (save-excursion
               (goto-char (car header-region))
               (let* ((new (get-text-property 0 keyword topic))
                      (newtext (cond
                                ((listp new) (mapconcat #'identity new ", "))
                                ((stringp new) new)
                                (t new))))
                               (cond
                                ((re-search-forward (concat "^.*" header ": \\(?1:.*\\)?") (cdr header-region) t)
                               (replace-match newtext nil nil nil 1))
                              (t
                               (goto-char (cdr header-region))
                               (if (re-search-backward "^.*:.*?$" (point-min) t)
                               (goto-char (line-end-position))
                               (goto-char (car header-region)))
                               (pcase-let* ((inhibit-read-only t)
                                            (`(,_title-marker ,header-marker)
                                             (consult-gh-topics--markers-for-metadata)))
                                 (if (not (looking-back "\n" (- (point) 1))) (insert "\n"))
                                  (consult-gh-topics--insert-field-header-string (concat header-marker header ": "))
                                 (insert newtext))))))))

(defun consult-gh-topics--format-field-cursor-intangible (string &optional prefix suffix)
  "Make a read-only field header from STRING.

When optional arguments PREFIX, or SUFFIX are non-nil, add them
as normal text without propeties before or after STRING.

This is useful to create non-editable fields for forms such as
“Title: ” or “Date: ” in the line."
  (concat prefix
          (propertize (substring string 0 -1) 'cursor-intangible t)
          (propertize (substring string -1) 'cursor-intangible t 'rear-nonsticky t)
          suffix))

(defun consult-gh-topics--markers-for-metadata ()
  "Get markers depending on `consult-gh-topic-major-mode'."
  (pcase consult-gh-topic-major-mode
    ('gfm-mode
     (list "# " "> "))
    ('markdown-mode
     (list "# " "> "))
    ('org-mode
     (list "#+" "#+"))
    (_
     (list "# " "> "))))

(defun consult-gh-topics--code-comment-submit (comment repo number &optional commit-id path line side startline startside)
  "Submit a comment on LINE at PATH for pull request NUMBER in REPO.

Description of Arguments

  COMMENT    a string; comment on code in LINE at PATH in REPO
  REPO       a string; full name of repository
  NUMBER     a string; number of pull request
  COMMIT-ID  a string; sha of the commit to comment on
  PATH       a string; path to a file in repo
  LINE       a number; number of line at path to comment on
  SIDE       a string; side off diff to comment on
  STARTLINE  a number; start line for multi-line comments
  STARTSIDE  a string; start side for multi-line comments"
  (let* ((args (list "api" "-X" "POST")))
    (if (string-empty-p comment)
        (message "Comment cannot be empty!")
      (progn
        (setq args (append args
                           (and comment (list "-f" (concat "body="  comment)))
                           (and path (list "-f" (concat "path="  path )))
                           (and startline (list "-F" (concat "start_line=" (number-to-string startline))))
                           (and startside (list "-f" (concat "start_side=" startside)))
                           (and line (list "-F" (concat "line=" (number-to-string line))))
                           (and side (list "-f" (concat "side=" side)))
                           (and commit-id (list "-f" (concat "commit_id=" commit-id)))
                           (list (format "repos/%s/pulls/%s/comments" repo number))))
        (apply #'consult-gh--command-to-string args)))))

(defun consult-gh-topics--file-comment-submit (comment repo number &optional commit-id path)
  "Add a COMMENT on file in pull request NUMBER in REPO.

Description of Arguments

  COMMENT    a string; text of comment on file at PATH in REPO
  REPO       a string; full name of repository
  NUMBER     a string; id number of pull request
  COMMIT-ID  a string; sha of commit to comment on
  PATH       a string; path to a file in repo"
  (let* ((args (list "api" "-X" "POST")))
    (if (string-empty-p comment)
        (message "Comment cannot be empty!")
      (progn
        (setq args (append args
                           (and comment (list "-f" (concat "body="  comment)))
                           (and commit-id (list "-f" (concat "commit_id=" commit-id)))
                           (and path (list "-f" (concat "path="  path )))
                           (list "-f" "subject_type=file")
                           (list (format "repos/%s/pulls/%s/comments" repo number))))
        (apply #'consult-gh--command-to-string args)))))

(defun consult-gh-topics--reply-comment-submit (comment comment-id reply-url)
  "Add a COMMENT in reply to COMMENT-ID pull request.

Description of Arguments

  COMMENT    a string; body text of comment
  COMMENT-ID a string; id of the comment replying to
  REPLY-URL  a string; the GitHub api url to send the respond to"
  (let* ((args (list "api" "-X" "POST")))
    (if (string-empty-p comment)
        (message "Comment cannot be empty!")
      (progn
        (setq args (append args
                           (and comment (list "-f" (concat "body="  comment)))
                           (and comment-id (list "-F" (concat "in_reply_to=" (number-to-string comment-id))))
                           (list reply-url)))
        (apply #'consult-gh--command-to-string args)))))

(defun consult-gh-topics--topic-comment-submit (&optional comment repo target number)
  "Submit the COMMENT on topic of TYPE and NUMBER in REPO.

This command submits the content of the COMMENT string github api for topic
of TYPE (e.g. issue, pr, ...) and id NUMBER.

Description of Arguments:
  REPO   a string; full name of target repository
  TARGET a string; TARGET topic (e.g. issue, pr, review ...)
  NUMBER a string; id number for issue, pr, or ..."
  (let* ((args (list "api" "-X" "POST"))
         (repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
         (target (or target (consult--read  (list (cons "Issues" "issue") (cons "Pull Requests" "pr"))
                                        :prompt "What topic are you looking for? "
                                        :lookup #'consult--lookup-cdr
                                        :require-match t
                                        :sort nil)))
         (number (or number (pcase target
                              ("issue" (get-text-property 0 :number (consult-gh-issue-list repo t)))
                              ("pr" (get-text-property 0 :number (consult-gh-pr-list repo t))))))
         (comment (or comment (consult--read nil :prompt "Comment: "))))

    (if (string-empty-p comment)
        (progn
          (message "Comment cannot be empty!")
          nil)
      (pcase target
        ((or "issue" "pr")
          (apply #'consult-gh--command-to-string (append args (list (format "repos/%s/issues/%s/comments" repo number) "-f" (format "body=%s" comment)))))
        ("discussion"
         (message "Commenting on discussions is not supported, yet!")
         nil)))))

(defun consult-gh-topics--commit-submit (&optional commit)
  "Sbmit COMMIT."
  (let* ((commit (or commit
                  (and (stringp consult-gh--topic)
                       (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                         consult-gh--topic)))

         (target (get-text-property 0 :commit-target commit)))
            (cond
             ((equal target "create")
              (consult-gh--create-commit-presubmit commit))
             ((equal target "delete")
              (consult-gh--delete-commit-presubmit commit))
             ((equal target "upload")
              (consult-gh--upload-commit-presubmit commit))
             ((equal target "rename")
              (consult-gh--rename-commit-presubmit commit)))))

(defun consult-gh--repo-get-branches-json (repo)
  "List branches of REPO, in json format.

uses `consult-gh--api-get-json' to get branches from GitHub API."
  (consult-gh--api-get-json (concat "repos/" repo "/branches")))

(defun consult-gh--repo-get-branches-hashtable-to-list (table repo)
  "Convert TABLE with branches of REPO to a list of propertized text."
  (mapcar (lambda (item)
            (when (hash-table-p item)
              (let* ((name (gethash :name item))
                     (api-url (gethash :url (gethash :commit item)))
                     (sha (gethash :sha (gethash :commit item)))
                     (protected (gethash :protected item)))
                  (when (stringp name)
                    (propertize name
                               :repo repo
                               :ref name
                               :api-url api-url
                               :sha sha
                               :protected protected
                               :type "branch"
                               :class "branch")))))
          table))

(defun consult-gh--repo-get-branches-list (repo &optional set-valid-branches)
  "Return REPO's information in propertized text format.

When SET-VALID-BRANCHES in non-nil, set :valid-refs property of
the buffer-local variable for `consult-gh--topic'."
  (let* ((topic consult-gh--topic)
         (items (consult-gh--json-to-hashtable  (consult-gh--api-get-command-string (format "repos/%s/branches" repo))))
         (branches (consult-gh--repo-get-branches-hashtable-to-list items repo)))

(when (and set-valid-branches (stringp topic) branches)
      (add-text-properties 0 1 (list :valid-refs branches) topic))

branches))

(defun consult-gh--repo-get-default-branch (repo)
  "Return REPO's default branch."
(consult-gh--json-to-hashtable
 (consult-gh--api-get-command-string (format "/repos/%s" repo))
 :default_branch))

(defun consult-gh--read-branch (repo &optional initial prompt require-match create-new predicate)
  "Query the user to select a branch of REPO.

Description of Arguments:
  REPO          a string; full repository name
                \(e.g., “armindarvish/consult-gh”\)
  INITIAL       a string; initial value to be passed to `consult--read'.
  PROMPT        a string; prompt to be passed to `consult--read'.
  REQUIRE-MATCH a boolean; is passed to `consult--read'.
  CREATE-NEW    a boolean; whether to create a new branch if the user
                enters a new non-existing name.  The user will be
                asked for confirmation before creating one.
  PREDICATE     a function; a filter function passed to `consult--read'."
  (let* ((candidates (consult-gh--repo-get-branches-list repo))
         (sel (consult--read candidates
                             :prompt (or prompt (concat "Select Branch for "
                                                        (propertize (format "\"%s\"" repo) 'face 'consult-gh-default)
                                                        ": "))
                             :category 'consult-gh-branches
                             :require-match require-match
                             :initial initial
                             :add-history  (let* ((topicbranch
                                                   (and consult-gh--topic
                                                        (stringp consult-gh--topic)
                                                        (get-text-property 0 :ref consult-gh--topic))))
                                             (append (list
                                                      topicbranch
                                                      (thing-at-point 'symbol))))
                             :lookup (lambda (sel cands &rest _args)
                                       (or (car-safe (member sel cands)) sel))
                             :sort t
                             :predicate predicate)))
    (if (get-text-property 0 :ref sel)
        sel
      (when (and create-new
                 (y-or-n-p "That branch does not exit.  Do you want to make a new branch?"))
        (consult-gh-branch-create repo nil (substring-no-properties sel) t))
      sel)))

(defun consult-gh--branch-create-suggest-name (repo user)
"Suggest a name for new branch created in REPO by USER."
(let* ((branches (consult-gh--repo-get-branches-list repo))
       (name (format "%s-patch-" user))
       (existing (all-completions name branches))
       (last-branch (if existing (car (sort existing
                            (lambda (x y)
                              (let ((x-num (string-remove-prefix name x))
                                    (y-num (string-remove-prefix name y)))
                                (if (< (string-to-number x-num) (string-to-number y-num))
                                    nil
                                  t)))))))
       (last-num (if (stringp last-branch)
                     (string-to-number (string-remove-prefix name last-branch))
                   0)))
  (when (numberp last-num)
    (format "%s%s" name (+ last-num 1)))))

(defun consult-gh--branch-create (repo &optional ref branch-name sync)
  "Create a new branch in REPO from REF called BRANCH-NAME.

If optional argument SYNC is non-nil, run the prcess synchronously."
  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (canwrite (consult-gh--user-canwrite repo))
         (_  (unless canwrite
               (user-error "Current user, %s, does not have permissions to create a branch in %s" user repo)))
         (initial (or (consult-gh--branch-create-suggest-name repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
         (existing-branches (consult-gh--repo-get-branches-list repo))
         (branch-name (or branch-name
                          (consult--read (list initial)
                                         :prompt "Name of the new branch: "
                                         :sort nil)))
         (tries (if (stringp branch-name) 1))
         (branch-name (if (and (stringp branch-name)
                               (member branch-name existing-branches))
                          (while (< tries 3)
                            (cl-incf tries)
                            (consult--read (list initial)
                                         :prompt (format "A branch with that name already exists. Pick a new name (attempt %s/3): " tries)
                                         :sort nil))
                        branch-name))
         (_ (if (not branch-name) (user-error "Did not get a valid branch name")))
         (ref (or ref (consult-gh--read-branch repo nil "Select reference branch for starting point: " t nil)))
         (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                  (and (stringp ref)
                       (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
         (args (list "api" "-X" "POST")))
    (when (and repo branch-name sha)
      (setq args (append args (list (format "/repos/%s/git/refs" repo)
                                    "-f" (format "ref=refs/heads/%s" branch-name)
                                    "-f" (format "sha=%s" sha))))
      (if sync
          (and (apply #'consult-gh--command-to-string  args)
               (message "branch %s %s" (propertize branch-name 'face 'consult-gh-branch) (propertize "created!" 'face 'consult-gh--success))
                     branch-name)
        (and (consult-gh--make-process "consult-gh-create-branch"
                                       :when-done (lambda (_event _str)
                                                    (message "branch %s %s" (propertize branch-name 'face 'consult-gh-branch) (propertize "created!" 'face 'consult-gh--success)))
                                      :cmd-args args)
             branch-name)))))

(defun consult-gh--branch-delete (repo branch &optional sync)
  "Delete BRANCH in REPO.

If optional argument SYNC is non-nil, run the prcess synchronously."
  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (canwrite (consult-gh--user-canwrite repo))
         (_  (unless canwrite
               (user-error "Current user, %s, does not have permissions to create a branch in %s" user repo)))
         (branch (or branch
                     (get-text-property 0 :ref (consult-gh--read-branch repo nil "Select the branch to delete: " t nil))))
         (args (list "api" "-X" "DELETE")))
    (when (and repo branch)
      (setq args (append args (list (format "/repos/%s/git/refs/heads/%s" repo branch))))
      (if sync
          (and (y-or-n-p (format "This will delete branch %s.  Are you sure you want to continue? "
 (propertize branch 'face 'consult-gh-warning)))
                (apply #'consult-gh--command-to-string  args)
               (message "branch %s %s" (propertize branch 'face 'consult-gh-branch) (propertize "deleted!" 'face 'consult-gh-error))
                     branch)
        (and (y-or-n-p (format "This will delete branch %s.  Are you sure you want to continue? "
 (propertize branch 'face 'consult-gh-warning)))
             (consult-gh--make-process "consult-gh-delete-branch"
                                       :when-done (lambda (_event _str)
                                                    (message "branch %s %s" (propertize branch 'face 'consult-gh-branch) (propertize "deleted!" 'face 'consult-gh-error)))
                                       :cmd-args args)
             branch)))))

(defun consult-gh--repo-get-tags (repo &optional set-valid-tags)
  "List tags of REPO.

When SET-VALID-TAGS in non-nil, set :valid-release-tags property of
the buffer-local variable for `consult-gh--topic'."
  (let* ((topic consult-gh--topic)
         (table  (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (concat "repos/" repo "/tags"))))
         (tags  (when (listp table)
                  (mapcar (lambda (item)
                            (when (hash-table-p item)
                              (if-let* ((name (gethash :name item)))
                                  (when (stringp name)
                                    (propertize name
                                                :repo repo
                                                :tagname name
                                                :tarball-api-url (gethash :tarball_url item)
                                                :zipball-api-url (gethash :zipball_url item)
                                                :sha (ignore-errors (gethash :sha (gethash :commit item)))
                                                :type "tag")))))
                          table))))
    (when (and (stringp topic) set-valid-tags)
      (add-text-properties 0 1 (list :valid-release-tags tags) topic))
    tags))

(defun consult-gh--read-tag (repo &optional initial prompt require-match)
"Query the user to select a tag of REPO.

REPO must be a Github repository full name
for example “armindarvish/consult-gh”.

INITIAL is passed as :initial to `consult--read'.

REQUIRE-MATCH is passed as :require-match to `consult--read'.

If PROMPT is non-nil, use it as the query prompt"
(let* ((candidates (consult-gh--repo-get-tags repo))
       (sel (consult--read candidates
                           :prompt (or prompt "Select a Tag: ")
                           :category 'consult-gh-tags
                           :require-match require-match
                           :initial initial
                           :lookup (lambda (sel cands &rest _args)
                                     (or (car-safe (member sel cands))
                                       sel)))))
  (if (get-text-property 0 :name sel)
      sel
  (and (y-or-n-p "That tag does not exit.  Do you want to make a new tag?")
                 sel))))

(defun consult-gh--read-ref (repo &optional initial prompt require-match ref-type history default create-new)
  "Get a branch or tag of REPO.

Description of Arguments:
  REPO          a string; repository's full name
                \(e.g., armindarvish/consult-gh\)
  INITIAL       a string; used as initial input in searching refs
                \(gets passed to `consult--multi'\).
  PROMPT        a string; prompt to use when selecting refs.
                \(gets passed to `consult-multi'\).
  REQUIRE-MATCH a boolean; whether to require match
                \(gets passed to `consult-multi'\).
  REF-TYPE      a symbol; either nil, \='branch or \='tag
                when non-nil limit the selection to this type
  HISTORY       a symbol; history variable passed to `consult-multi'.
  DEFAULT       a string; default value passed to `consult-multi'.
  CREATE-NEW    a boolean; whether to create a new branch if the user
                enters a new non-existing name.  The user will be
                asked for confirmation before creating one."
  (let*  ((branches (consult-gh--repo-get-branches-list repo))
          (tags (consult-gh--repo-get-tags repo))
          (candidates (pcase ref-type
                        ('nil (list
                                (list :name "Branch"
                                      :items branches
                                      :sort t)
                                (list :name "Tag"
                                      :items tags
                                      :sort nil)))
                        ('branch (list
                                  (list :name "Branch"
                                        :items branches
                                        :sort t)))
                        ('tag (list
                               (list :name "Tag"
                                     :items tags
                                     :sort nil)))))

          (sel (consult--multi candidates
                               :prompt (or prompt (concat "Select Reference for "
                                                          (propertize (format "\"%s\"" repo) 'face 'consult-gh-default)
                                                          ": "))
                               :category 'consult-gh-branches
                               :require-match require-match
                               :initial initial
                               :add-history  (let* ((topicbranch
                                                     (and consult-gh--topic
                                                          (stringp consult-gh--topic)
                                                              (get-text-property 0 :ref consult-gh--topic))))
                                               (append (list
                                                        topicbranch
                                                        (thing-at-point 'symbol))))
                               :history history
                               :default default
                               :sort nil)))
    (when (listp sel)
        (cond
         ((equal (plist-get (cdr sel) :match) nil)
          (if (and create-new
                   (y-or-n-p "That reference does not exit.  Do you want to make a new branch?"))
              (consult-gh-branch-create repo nil (substring-no-properties (car sel)) t))
          (car sel))
         ((equal (plist-get (cdr sel) :name) "Branch")
          (car sel))
         ((equal (plist-get (cdr sel) :name) "Tag")
          (car sel))))))

(defun consult-gh--commit-get-diff-buffer (&optional commit)
  "Ger diff buffer for COMMIT.

COMMIT is a string with properties that identify a commit.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--commit-create'."
  (let* ((commit (or commit consult-gh--topic))
         (repo (get-text-property 0 :repo commit))
         (ref (or (get-text-property 0 :commit-ref commit)
                  (get-text-property 0 :ref commit)))
         (target (get-text-property 0 :commit-target commit)))
      (let* ((files (get-text-property 0 :files commit))
             (buffername (format "consult-gh-diff-%s:%s/%s" target repo ref))
             (content
              (when (listp files)
                (consult--slow-operation "Collecting file contents. This may take a while!"
                  (cl-loop for file in files
                           collect
                           (let* ((file-diff-buff (get-text-property 0 :diff-buffer file))
                                  (file-diff-done (get-text-property 0 :diff-done file)))
                             (and file-diff-done
                                  (bufferp file-diff-buff)
                                  (buffer-live-p file-diff-buff))
                             (with-current-buffer file-diff-buff
                               (buffer-string)))))))
             (diff-content (when (listp content)
                             (mapconcat #'identity content "\n\n"))))

        (with-current-buffer (get-buffer-create buffername)
          (let ((inhibit-read-only t))
            (erase-buffer)
            (insert diff-content)
            (set-buffer-file-coding-system 'raw-text)
            (set-buffer-multibyte t)
            (diff-mode)
            (add-to-list 'consult-gh--preview-buffers-list (current-buffer))
            (add-text-properties 0 1 (list :diff-buffer (current-buffer)) commit)
            (current-buffer))))))

(defun consult-gh--commit-get-buffer-message ()
  "Get the commit message in the buffer.

This is adapted from magit:
URL `https://github.com/magit/magit/blob/731642756f504c8a56d3775960b7af0a93c618bb/lisp/git-commit.el#L818'"
  (let ((flush (concat "^" comment-start))
        (str (buffer-substring-no-properties (point-min) (point-max))))
    (with-temp-buffer
      (insert str)
      (goto-char (point-min))
      (when (re-search-forward (concat flush " -+ >8 -+$") nil t)
        (delete-region (line-beginning-position) (point-max)))
      (goto-char (point-min))
      (flush-lines flush)
      (goto-char (point-max))
      (unless (eq (char-before) ?\n)
        (insert ?\n))
      (setq str (buffer-string)))
    (and (not (string-match "\\`[ \t\n\r]*\\'" str))
         (progn
           (when (string-match "\\`\n\\{2,\\}" str)
             (setq str (replace-match "\n" t t str)))
           (when (string-match "\n\\{2,\\}\\'" str)
             (setq str (replace-match "\n" t t str)))
           str))))

(defun consult-gh--commit-change-committer (&optional commit)
  "Change committer for COMMIT.

COMMIT is a string with properties that identifies a commit.  For an
example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--commit-create'."
  (let* ((commit (or commit
                   (and (stringp consult-gh--topic)
                        (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                        consult-gh--topic)))
         (committer-info (or (get-text-property 0 :committer-info commit)
                             (consult-gh--get-user-info (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))))
         (committer-name (when (hash-table-p committer-info) (gethash :name committer-info)))
         (committer-email (when (hash-table-p committer-info) (gethash :email committer-info))))
    (setq committer-name (consult--read (list committer-name)
                                        :prompt "Enter New Committer's Name: "
                                        :default committer-name
                                        :sort nil))
    (setq committer-email (consult--read (list committer-email)
                                         :prompt "Enter New Committer's Email: "
                                         :default committer-email
                                         :sort nil))
    (puthash :name committer-name committer-info)
    (puthash :email committer-email committer-info)
    (add-text-properties 0 1 (list :committer-info committer-info) commit)))

(defun consult-gh--commit-change-author (&optional commit)
  "Change author of COMMIT.

COMMIT is a string with properties that identifies a file.  For an
example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--commit-create'."
  (let* ((commit (or commit
                   (and (stringp consult-gh--topic)
                        (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                        consult-gh--topic)))
         (author-info (or (get-text-property 0 :author-info commit)
                          (consult-gh--get-user-info (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))))
         (author-name (when (hash-table-p author-info) (gethash :name author-info)))
         (author-email (when (hash-table-p author-info) (gethash :email author-info))))
    (setq author-name (consult--read (list author-name)
                                        :prompt "Enter New Author's Name: "
                                        :default author-name
                                        :sort nil))
    (setq author-email (consult--read (list author-email)
                                         :prompt "Enter New Author's Email: "
                                         :default author-email
                                         :sort nil))
    (puthash :name author-name author-info)
    (puthash :email author-email author-info)
    (add-text-properties 0 1 (list :author-info author-info) commit)))

(defun consult-gh--commit-change-ref (&optional commit)
  "Change branch of commit.

COMMIT is a string with properties that identifies a commit.  For an
example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--commit-create'."
  (let* ((commit (or commit
                   (and (stringp consult-gh--topic)
                        (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                        consult-gh--topic)))
         (repo  (get-text-property 0 :repo commit))
         (ref (or (get-text-property 0 :commit-ref commit)
                     (get-text-property 0 :ref commit))))
    (setq ref (or (consult-gh--read-branch repo nil "Select New Ref (i.e. Branch): " nil t) ref))
    (add-text-properties 0 1 (list :commit-ref ref) commit)))

(defun consult-gh--commit-create-make-diff-buffer (repo file &optional ref)
  "Make a diff buffer for a commit to create a FILE in REPO.

REF is the name of a branch or tag name."
  (let* ((viewbuffer (get-text-property 0 :view-buffer file))
         (local-path (get-text-property 0 :local-path file))
         (target "create")
         (new-content (or (and (bufferp viewbuffer)
                               (buffer-live-p viewbuffer)
                               (with-current-buffer viewbuffer
                                 (save-restriction
                                   (widen)
                                   (buffer-substring-no-properties (point-min) (point-max)))))
                          (and (file-exists-p local-path)
                               (with-temp-buffer (insert-file-contents local-path)
                                                 (buffer-string)))))
         (base64-content (and (stringp new-content)
                              (base64-encode-string (substring-no-properties (encode-coding-string new-content 'utf-8)))))
         (path (get-text-property 0 :path file))
         (url (format "/repos/%s/contents/%s" repo path))
         (url-ref (if ref (concat url (format "?ref=%s" ref)) url))
         (diff-buff (get-buffer-create (format " *consult-gh-diff-create-%s-%s-%s:%s*" target repo ref path))))
    (with-current-buffer diff-buff
      (let ((inhibit-read-only t))
        (erase-buffer)
        (add-text-properties 0 1 (list :diff-done nil) file))
      (add-text-properties 0 1 (list :diff-buffer diff-buff) file))
    (consult-gh--make-process (format "consult-gh-diff-create:%s/%s/%s" repo ref path)
                              :on-error
                              (lambda (_event _str)
                                (with-temp-buffer
                                  (insert (or new-content ""))
                                  (set-buffer-file-coding-system 'raw-text)
                                  (set-buffer-multibyte t)
                                  (let ((new-buffer (current-buffer)))
                                    (with-temp-buffer
                                      (insert "")
                                      (set-buffer-file-coding-system 'raw-text)
                                      (set-buffer-multibyte t)
                                      (let ((old-buffer (current-buffer)))
                                        (with-temp-buffer
                                          (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                          (let ((diff-str (buffer-string)))
                                            (when (stringp diff-str)
                                              (with-current-buffer diff-buff
                                                (goto-char (point-max))
                                                (insert (concat (format "Creating %s:\n\n" path)
                                                                diff-str
                                                                "\n\n"))))
                                            (add-text-properties 0 1 (list :diff-done t) file))))))))
                              :when-done
                              (lambda (_event str)
                                (let* ((content (when (stringp str) (consult-gh--json-to-hashtable str :content)))
                                       (old-content (when (stringp content)
                                                      (base64-decode-string content))))
                                  (with-temp-buffer
                                    (insert (or new-content ""))
                                    (set-buffer-file-coding-system 'raw-text)
                                    (set-buffer-multibyte t)
                                    (let ((new-buffer (current-buffer)))
                                      (with-temp-buffer
                                        (insert (or old-content ""))
                                        (set-buffer-file-coding-system 'raw-text)
                                        (set-buffer-multibyte t)
                                        (let ((old-buffer (current-buffer)))
                                          (with-temp-buffer
                                            (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                            (let ((diff-str (buffer-string)))
                                              (when (stringp diff-str)
                                                (with-current-buffer diff-buff
                                                  (goto-char (point-max))
                                                  (insert (concat (format "Updating %s:\n\n" path)
                                                                  diff-str
                                                                  "\n\n"))))
                                              (add-text-properties 0 1 (list :diff-done t) file)))))))))
                              :cmd-args (list "api" "-H" "Accept: application/vnd.github+json" "--paginate" url-ref))
    (add-text-properties 0 1 (list :base64-content base64-content) file)))

(defun consult-gh--create-commit (files repo ref &optional commit-message)
  "Create commit to make/update FILES in REPO and REF.

REPO is full name of a GitHub repository.  REF is the name of a branch
or tag name.  FILES is a list of strings with properties that identify
a file.  For an example, see the buffer-local variable
`consult-gh--topic' in the buffer generated by
`consult-gh--files-view'.  It defaults to
`consult-gh--topic' in the current buffer.

It opens a buffer to enter the commit message for editing FILES.
If COMMIT-MESSAGE is non-nil, it is inserted in the buffer as initial
message."
  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (committer-info (consult-gh--get-user-info user))
         (author-info (consult-gh--get-user-info user))
         (buffer (format "*consult-gh-file-edit-commit-message: %s/%s" repo ref))
         (newtopic (format "%s/%s" repo ref))
         (target "create")
         (type "commit message"))

    (add-text-properties 0 1 (list :isComment nil :type type :new t :number nil :committer-info committer-info :author-info author-info :commit-ref ref :content nil :commit-target target :repo repo :ref ref :files files) newtopic)

    ;; insert commit message
    (consult-gh-topics--get-buffer-create buffer "commit message" newtopic)
    (with-current-buffer buffer
      (consult-gh-commit-message-mode +1)
      (save-excursion
        (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions)))
          (goto-char (or (car-safe (car-safe regions)) (point-min))))
        (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
        (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
        (when (listp files)
          (insert (propertize "\n\n" :consult-gh-commit-instructions t))
          (insert (propertize "# Files to Create/Update:" :consult-gh-commit-instructions t))
          (mapc (lambda (f)
                  (consult-gh--commit-create-make-diff-buffer repo f ref)
                  (add-text-properties 0 1 (list :commit-buffer (get-buffer buffer)) f)
                  (insert (propertize (format "\n# - create/update %s" f) :consult-gh-commit-instructions t)))
                files))
        (goto-char (point-min))
        (when (stringp commit-message)
          (insert commit-message)
          (consult-gh-commit-save-message))
        (funcall consult-gh-pop-to-buffer-func buffer)))))

(defun consult-gh--create-commit-by-pullrequest (files repo &optional commit-message ref committer-info author-info)
"Submit commit by creating a pull request.

This is used when REF is protected and does not allow direct
commits.  A new branch is created and a pull request is made to merge
the new branch to REF.

Description of Arguments:
  FILES          a list of propertized strings; each string contains
                 information \(e.g. path, content, ...) for 1 file.
  REPO           a string; full name of repository
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (initial (or (consult-gh--branch-create-suggest-name repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
         (existing-branches (consult-gh--repo-get-branches-list repo))
         (new-ref (consult--read (list initial)
                                         :prompt "Name of the new branch: "
                                         :sort nil))
         (tries (if (stringp new-ref) 1))
         (new-ref (if (and (stringp new-ref)
                               (member new-ref existing-branches))
                      (while (< tries 3)
                        (cl-incf tries)
                        (consult--read (list initial)
                                         :prompt (format "A branch with that name already exists. Pick a different name (attempt %s/3): " tries)
                                         :sort nil))
                        new-ref))
         (_ (if (not new-ref) (user-error "Did not get a valid branch name")))
         (ref (or ref (consult-gh--read-branch repo nil "Select reference branch for starting point: " t nil)))
         (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                  (and (stringp ref)
                       (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
         (args (list "api" "-X" "POST")))
    (when (and repo new-ref sha)
      (setq args (append args (list (format "/repos/%s/git/refs" repo)
                                    "-f" (format "ref=refs/heads/%s" new-ref)
                                    "-f" (format "sha=%s" sha))))

      (consult-gh--make-process (format "consult-gh-create-branch-%s" repo)
                                :when-done (lambda (_event _str)
                                              (and
                                               (consult-gh--create-commit-submit files repo commit-message new-ref committer-info author-info)
                                               (message "Creating a Pull Request...")
                                               (consult-gh-pr-create repo "Update Files" nil ref repo new-ref)))
                                      :cmd-args args))))

(defun consult-gh--create-commit-by-fork (files repo &optional commit-message ref committer-info author-info)
  "Submit commit by forking REPO and creating a pull request.

This is used when user does not have write access in REPO.  The REPO is
first forked, then the changes are committed in the fork and pull
request is made to merge the new branch in the fork back to REF in
REPO.

Description of Arguments:
  FILES          a list of propertized strings; each string contains
                 information \(e.g. path, content, ...) for 1 file.
  REPO           a string; full name of repository to fork
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (name (consult-gh--get-package repo))
         (new-repo (concat user "/" name))
         (existing (consult-gh--command-to-string "repo" "view" new-repo "--json" "\"url\"")))
    (cond
     ((not existing)
      (consult-gh--make-process (format "consult-gh-fork-%s" repo)
                              :when-done (lambda (_event _str)
                                            (and
                                             (message "repo %s was forked to %s"
                                                      (propertize repo 'face 'font-lock-keyword-face)
                                                      (propertize new-repo 'face 'font-lock-warning-face))
                                             (consult-gh--create-commit-submit files new-repo commit-message ref committer-info author-info)
                                             (message "Creating a Pull Request...")
                                             (consult-gh-pr-create repo "Create/Update Files" nil ref new-repo ref)))
                              :cmd-args (list "repo" "fork" (format "%s" repo) "--fork-name" name)))
     (t
      (message "Forked repo already exists. Making a new branch...")
      (let* ((initial (or (consult-gh--branch-create-suggest-name new-repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
             (existing-branches (consult-gh--repo-get-branches-list new-repo))
             (new-ref (consult--read (list initial)
                                         :prompt "Name of the new branch: "
                                         :sort nil))
             (tries (if (stringp new-ref) 1))
             (new-ref (if (and (stringp new-ref)
                               (member new-ref existing-branches))
                          (while (< tries 3)
                            (cl-incf tries)
                            (consult--read (list initial)
                                         :prompt (format "A branch with that name already exists. Pick a different name (attempt %s/3): " tries)
                                         :sort nil))
                        new-ref))
             (_ (if (not new-ref) (user-error "Did not get a valid branch name")))
             (ref (or ref (consult-gh--read-branch new-repo nil "Select reference branch for starting point: " t nil)
                      "HEAD"))
             (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                      (and (stringp ref)
                           (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
             (args (list "api" "-X" "POST")))
         (when (and new-repo new-ref sha)
           (setq args (append args (list (format "/repos/%s/git/refs" new-repo)
                                    "-f" (format "ref=refs/heads/%s" new-ref)
                                    "-f" (format "sha=%s" sha))))

           (consult-gh--make-process (format "consult-gh-create-branch-%s" new-repo)
                                :when-done (lambda (_event _str)
                                              (and
                                               (consult-gh--create-commit-submit files new-repo commit-message new-ref committer-info author-info)
                                               (message "Creating a Pull Request...")
                                               (consult-gh-pr-create repo "Create/Updates" nil ref new-repo new-ref)))
                                      :cmd-args args)))))))

(defun consult-gh--create-commit-single-file (file repo &optional commit-message ref committer-info author-info api-response)
  "Submit a commit to create/update a single file.

Description of Arguments:
  FILE           a string with properties; the properties describe details
                 abount path and content of the file.
  REPO           a string; full name of repository
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)
  API-RESPONSE   a cons; api response from “repos/REPO/contents/FILE”
                 where car is the response code, and the cdr is the
                 response string"

  (let* ((repo (or repo (get-text-property 0 :repo file)))
         (content (get-text-property 0 :base64-content file))
         (file-path (get-text-property 0 :path file))
         (ref (or ref (get-text-property 0 :ref file)))
         (committer-name (when (hash-table-p committer-info) (gethash :name committer-info)))
         (committer-email (when (hash-table-p committer-info) (gethash :email committer-info)))
         (author-name (when (hash-table-p author-info) (gethash :name author-info)))
         (author-email (when (hash-table-p author-info) (gethash :email author-info)))
         (url (format "/repos/%s/contents/%s" repo file-path))
         (url-ref (if ref (concat url (format "?ref=%s" ref)) url))
         (api-response (or api-response (consult-gh--api-get-json url-ref)))
         (file-sha (if (eq (car api-response) 0)
                       (consult-gh--json-to-hashtable (cadr api-response) :sha)))
         (args (list "api"
                     "-H" "Accept: application/vnd.github+json"
                     "--method" "PUT"
                     url
                     "-f" (format "message=%s" commit-message)
                     "-f" (format "content=%s" content))))
    (when ref (setq args (append args (list "-f" (format "branch=%s" ref)))))
    (when (and committer-name committer-email)
      (setq args (append args (list "-f" (format "committer[name]=%s" committer-name)
                                    "-f" (format "committer[email]=%s" committer-email)))))
    (when (and author-name author-email)
      (setq args (append args (list "-f" (format "author[name]=%s" author-name)
                                    "-f" (format "author[email]=%s" author-email)))))


    (if file-sha
        (progn (setq args (append args (list "-f" (format "sha=%s" file-sha))))
               (and (apply #'consult-gh--command-to-string args)
               (message "File %s %s!" (propertize file-path 'face 'consult-gh-date)  (propertize "updated" 'face 'consult-gh-warning))))
      (progn
        (and
         (apply #'consult-gh--command-to-string args)
         (message "File %s %s!" (propertize file-path 'face 'consult-gh-date) (propertize "created" 'face 'consult-gh-success)))))))

(defun consult-gh--create-commit-submit (files repo commit-message &optional ref committer-info author-info)
  "Submit a create/update FILES commit in REPO.

Description of Arguments:
  FILES          a list of propertized strings; each string contains
                 information \(e.g. path, content, ...) for 1 file.
  REPO           a string; full name of repository
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"
  (pcase-let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
               (canwrite (consult-gh--user-canwrite repo))
               (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
               (commit-message (or (and (stringp commit-message)
                                        (not (string-empty-p (string-trim commit-message)))
                                        commit-message)
                                   (and (listp files)
                                        (format "Create/Update Files\n\n%s\n" (mapconcat (lambda (item) (format " - %s" (file-name-nondirectory item))) files "\n")))
                                   (consult--read nil
                                                  :prompt "Commit Message: "
                                                  :sort nil)))
               (protected (when ref (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/branches/%s" repo ref)) :protected))))

    (when (and commit-message
               (stringp commit-message)
               (not (string-empty-p commit-message)))
      (consult-gh-commit-save-message commit-message))

    (cond
     ((not canwrite)
      (let* ((info (consult-gh--command-to-string "repo" "view" repo "--json" "isFork,isPrivate"))
             (isForked (equal (consult-gh--json-to-hashtable info :isFork) 't))
             (isPrivate (equal (consult-gh--json-to-hashtable info :isPrivate) 't)))
        (when (and (not isForked) (not isPrivate))
          (and (y-or-n-p (format "User %s does not have write acccess in %s.  Do you want to fork the repo and make a pull request?" user repo))
               (consult-gh--create-commit-by-fork files repo commit-message ref committer-info author-info)))))
     ((equal protected t)
      (and (y-or-n-p "The branch you are trying to edit is protected.  Do you want to submit a pull request? ")
           (consult-gh--create-commit-by-pullrequest files repo commit-message ref committer-info author-info)))
     (t
      (when (and canwrite repo (listp files) commit-message)
        (let* ((confirm))
          (cl-loop for file in files
                   collect
                   (let* ((file-path (get-text-property 0 :path file))
                          (ref (or ref (get-text-property 0 :ref file)))
                          (url (and (stringp file-path)
                                    (format "/repos/%s/contents/%s" repo file-path)))
                          (url-ref (if ref (concat url (format "?ref=%s" ref)) url))
                          (api-response (consult-gh--api-get-json url-ref))
                          (file-exists (eq (car api-response) 0)))
                     (unless (equal confirm "all")
                       (setq confirm (read-answer (concat "This will " (if file-exists "overwrite the existing file at "
                                                                         "create a new file at ")
                                                          (format "%s on GitHub.  Are you sure you want to continue?" (propertize file 'face 'consult-gh-warning)))
                                                  '(("yes"  ?y "create/pdate the file")
                                                    ("no"   ?n "skip to the next file")
                                                    ("all"  ?! "accept all remaining without more questions (will overwrite existing files!)")
                                                     ("help" ?h "show help")
                                                     ("quit" ?q "exit")))))
                     (cond
                      ((or (equal confirm "yes")
                           (equal confirm "all"))

                       (consult-gh--create-commit-single-file file repo commit-message ref committer-info author-info api-response))
                      ((equal confirm "no")
                       (message "Skipped %s" (propertize file 'face 'consult-gh-error)))
                      (t (message "Canceled!")))))))))))

(defun consult-gh--create-commit-add-file (&optional commit file)
  "Add FILE to the COMMIT for creating/updating files."
  (if consult-gh-topics-edit-mode
      (let* ((commit (or commit consult-gh--topic))
             (repo (get-text-property 0 :repo commit))
             (ref (or (get-text-property 0 :commit-ref commit)
                      (get-text-property 0 :ref commit)))
             (new-file (or file
                           (consult-gh--files-read-file repo ref nil nil "File Name: " nil nil nil 'branch)))
             (path (and (stringp new-file)
                     (not (string-empty-p new-file))
                     (get-text-property 0 :path new-file)))
             (path (and (stringp path)
                     (not (string-empty-p path))
                     path))
             (existing (get-text-property 0 :existing new-file)))

        (cond
         ((and (stringp new-file)
               existing
               (equal (get-text-property 0 :object-type new-file) "tree"))
          (let* ((new-file (consult-gh--files-read-file repo ref (file-name-as-directory path) nil "File Name: " nil nil nil 'branch)))
            (consult-gh--create-commit-add-file commit new-file)))
      ((and (stringp new-file)
            existing
            (equal (get-text-property 0 :object-type new-file) "blob"))
       (pcase (consult--read (list (cons "Choose a different path" :reselect)
                                   (cons "Edit the existing file" :edit))
                             :prompt "That file already exists.  What do you want to do?"
                             :lookup #'consult--lookup-cdr
                             :require-match t
                             :sort nil)
         (':reselect
          (let* ((new-file (consult-gh--files-read-file repo ref (or (and (stringp (file-name-directory path)) (file-name-as-directory (file-name-directory path)))
                                                                     nil)
                                                        nil "File Name: " nil nil nil 'branch)))
            (consult-gh--create-commit-add-file commit new-file)))
         (':edit
          (consult-gh-edit-file new-file (current-buffer)))))
      (t
       (and repo path
            (let* ((buffer (current-buffer))
                   (tempdir (expand-file-name (concat repo "/" (or ref "HEAD") "/")
                                    (or consult-gh--current-tempdir (consult-gh--tempdir)))))
              (with-current-buffer (consult-gh--files-create-buffer repo path ref nil tempdir)
                (add-text-properties 0 1 (list :commit-buffer buffer) consult-gh--topic)))))))))

(defun consult-gh--create-commit-remove-file (&optional commit)
  "Remove file from the files to create/update in COMMIT."
  (if consult-gh-topics-edit-mode
      (let* ((commit (or commit consult-gh--topic))
             (files (get-text-property 0 :files commit))
             (new-files (remove (consult--read files
                                               :prompt "Remove Files: "
                                               :lookup #'consult--lookup-member)
                                files)))
        (add-text-properties 0 1 (list :files new-files)
                             commit)
        (add-text-properties 0 1 (list :diff-buffer nil) commit)
        (save-excursion
          (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                     (goto-char (car-safe (car-safe regions)))))
          (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
          (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
          (when (listp new-files)
            (insert (propertize "\n\n" :consult-gh-commit-instructions t))
            (insert (propertize "# Files to Create/Update:" :consult-gh-commit-instructions t))
            (mapc (lambda (f)
                    (insert (propertize (format "\n# - create/update %s" f) :consult-gh-commit-instructions t)))
                  new-files))))))

(defun consult-gh--create-commit-presubmit (&optional commit)
  "Prepare COMMIT to submit.

COMMIT is a string with properties that identify a commit.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--commit-create'."
  (if consult-gh-commit-message-mode
      (let* ((commit (or commit
                       (and (stringp consult-gh--topic)
                            (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                            consult-gh--topic)))
             (repo (get-text-property 0 :repo commit))
             (ref (or (get-text-property 0 :commit-ref commit)
                     (get-text-property 0 :ref commit)))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (user-info (consult-gh--get-user-info user))
             (committer-info (or (get-text-property 0 :committer-info commit) user-info))
             (committer (or (and (hash-table-p committer-info)
                                 (or (gethash :name committer-info)
                                     (gethash :email committer-info))
                                 (concat (gethash :name committer-info) " - " (gethash :email committer-info)))
                            user))
             (author-info  (or (get-text-property 0 :author-info commit)
                               user-info))
             (author (or (and (hash-table-p author-info)
                              (or (gethash :name author-info)
                                (gethash :email author-info))
                          (concat (gethash :name author-info) " - " (gethash :email author-info)))
                           user))
             (nextsteps (append (list (cons "Submit" :submit))
                                (list (cons "Add Files" :add))
                                (list (cons "Remove Files" :remove))
                                (list (cons (format "Change Branch (current: %s)" ref) :ref))
                                (list (cons (format "Change Committer (current: %s)" committer) :committer))
                                (list (cons (format "Change Author (current: %s)" author) :author))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil))

             (buffer (current-buffer)))

          (pcase next
            (':cancel)
            (':submit
             (let* ((files (get-text-property 0 :files consult-gh--topic))
                    (ref (or (get-text-property 0 :commit-ref consult-gh--topic)
                                (get-text-property 0 :ref consult-gh--topic)))
                    (commit-message (consult-gh--commit-get-buffer-message)))
               (and (consult-gh--create-commit-submit files repo commit-message ref  committer-info author-info)
                    (message "Commit %s" (propertize "Submitted!" 'face 'consult-gh-success))
                    (with-current-buffer buffer
                    (funcall consult-gh-quit-window-func t)))))
            (':add (consult-gh--create-commit-add-file commit))
            (':remove (consult-gh--create-commit-remove-file commit))
            (':committer (consult-gh--commit-change-committer)
                         (consult-gh--create-commit-presubmit))
            (':author (consult-gh--commit-change-author)
                      (consult-gh--create-commit-presubmit))
            (':ref (consult-gh--commit-change-ref)
                      (consult-gh--create-commit-presubmit))))
    (message "Not a Github commit buffer!")))

(defun consult-gh--commit-delete-make-diff-buffer (repo file &optional ref)
  "Make a diff buffer for a commit to delete a FILE in REPO.

REF is the name of a branch or tag name."
  (let* ((target "delete")
         (new-content "")
         (path (get-text-property 0 :path file))
         (type (get-text-property 0 :type file))
         (ref (or ref "HEAD"))
         (diff-buff (get-buffer-create (format " *consult-gh-diff-%s-%s-%s:%s*" target repo ref path))))
    (with-current-buffer diff-buff
      (erase-buffer)
      (add-text-properties 0 1 (list :diff-done nil) file))
    (add-text-properties 0 1 (list :diff-buffer diff-buff) file)
    (pcase type
      ("directory"
       (let* ((dir-path (and (stringp path) (file-name-as-directory path)))
              (files-list (consult-gh--files-nodirectory-items repo dir-path ref)))
         (mapc (lambda (f)
                 (let* ((f-path (and (consp f)
                                     (plistp (cdr f))
                                     (plist-get (cdr f) :path)))
                        (f-path (and (stringp f-path)
                                     (not (string-empty-p f-path))
                                     f-path))
                        (url (format "/repos/%s/contents/%s" repo f-path))
                        (url-ref (if ref (concat url (format "?ref=%s" ref)) url)))
                   (when (stringp f-path)
                     (add-text-properties 0 1 (list :diff-done nil) file)
                     (consult-gh--make-process (format "consult-gh-diff-delete:%s/%s/%s" repo ref f-path)
                                               :on-error
                                               (lambda (_event _str)
                                                 (ignore))
                                               :when-done
                                               (lambda (_event str)
                                                 (let* ((content (when (stringp str) (consult-gh--json-to-hashtable str :content)))
                                                        (old-content (when (stringp content)
                                                                       (base64-decode-string content))))
                                                   (if (equal old-content new-content)
                                                       (with-current-buffer diff-buff
                                                         (goto-char (point-max))
                                                         (insert (concat (format "Deleting %s:\n\n" f-path)
                                                                         "\n\n")))
                                                     (with-temp-buffer
                                                       (insert new-content)
                                                       (let ((new-buffer (current-buffer)))
                                                         (with-temp-buffer
                                                           (insert (or old-content ""))
                                                           (set-buffer-file-coding-system 'raw-text)
                                                           (set-buffer-multibyte t)
                                                           (let ((old-buffer (current-buffer)))
                                                             (with-temp-buffer
                                                               (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                                               (let ((diff-str (buffer-string)))
                                                                 (when (stringp diff-str)
                                                                   (with-current-buffer diff-buff
                                                                     (goto-char (point-max))
                                                                     (insert (concat (format "Deleting %s:\n\n" f-path)
                                                                                     diff-str
                                                                                     "\n\n")))
                                                                   (add-text-properties 0 1 (list :diff-done t) file)))))))))))
                                               :cmd-args (list "api" "-H" "Accept: application/vnd.github+json" "--paginate" url-ref)))))
               files-list)))
      ("file"
       (let* ((url (format "/repos/%s/contents/%s" repo path))
              (url-ref (if ref (concat url (format "?ref=%s" ref)) url)))
         (consult-gh--make-process (format "consult-gh-diff-create:%s/%s/%s" repo ref path)
                                   :on-error
                                   (lambda (_event _str)
                                     (ignore))
                                   :when-done
                                   (lambda (_event str)
                                     (let* ((content (when (stringp str) (consult-gh--json-to-hashtable str :content)))
                                            (old-content (when (stringp content)
                                                           (base64-decode-string content))))
                                       (with-temp-buffer
                                         (insert new-content)
                                         (let ((new-buffer (current-buffer)))
                                           (with-temp-buffer
                                             (insert (or old-content ""))
                                             (set-buffer-file-coding-system 'raw-text)
                                             (set-buffer-multibyte t)
                                             (let ((old-buffer (current-buffer)))
                                               (with-temp-buffer
                                                 (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                                 (let ((diff-str (buffer-string)))
                                                   (when (stringp diff-str)
                                                     (with-current-buffer diff-buff
                                                       (goto-char (point-max))
                                                       (insert (concat (format "Deleting %s:\n\n" path)
                                                                       diff-str
                                                                       "\n\n"))
                                                       (add-text-properties 0 1 (list :diff-done t) file)))))))))))
                                   :cmd-args (list "api" "-H" "Accept: application/vnd.github+json" "--paginate" url-ref))))
      (_
       (with-current-buffer diff-buff
         (goto-char (point-max))
         (insert (format "Deleting %s:\n\n" path)
                 "\n\n"))))))

(defun consult-gh--delete-commit (repo files &optional ref commit-message)
  "Create a commit to delete FILES in REPO and REF.

REF is the name of a branch for commit ref.

If COMMIT-MESSAGE is non-nil, it is inserted in the buffer as initial message."
   (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (committer-info (consult-gh--get-user-info user))
          (author-info (consult-gh--get-user-info user))
          (buffer (format "*consult-gh-file-delete-commit-message: %s/%s" repo ref))
          (newtopic (format "%s/%s" repo ref))
          (type "commit message"))

     (add-text-properties 0 1 (list :isComment nil :type type :new t :number nil :committer-info committer-info :author-info author-info :commit-ref ref :content nil :commit-target "delete" :repo repo :files files :ref ref) newtopic)

     ;; insert commit message
     (consult-gh-topics--get-buffer-create buffer "commit message" newtopic)
     (with-current-buffer buffer
       (consult-gh-commit-message-mode +1)
       (save-excursion
         (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                    (goto-char (car-safe (car-safe regions)))))
         (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
       (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
       (when (listp files)
         (insert (propertize "\n\n" :consult-gh-commit-instructions t))
         (insert (propertize "# Files to delete:" :consult-gh-commit-instructions t))
         (mapc (lambda (f)
                 (consult-gh--commit-delete-make-diff-buffer repo f ref)
                 (insert (propertize (format "\n# - delete %s" f) :consult-gh-commit-instructions t)))
                 files)))
       (goto-char (point-min))
       (when (stringp commit-message)
         (insert commit-message)
         (consult-gh-commit-save-message)))
     (funcall consult-gh-pop-to-buffer-func buffer)))

(defun consult-gh--delete-commit-add-files (repo &optional ref commit)
  "Add files from REPO and REF to the files to be deleted in COMMIT."
  (if consult-gh-topics-edit-mode
      (let* ((files (get-text-property 0 :files commit))
             (new-file (consult-gh--files-read-file repo ref nil nil "File Name: " t t nil 'branch))
             (type (get-text-property 0 :object-type new-file))
             (new-path (and (stringp new-file)
                        (not (string-empty-p new-file))
                        (get-text-property 0 :path new-file)))
             (new-path (and (stringp new-path)
                        (not (string-empty-p new-path))
                        new-path))
             (_ (when (stringp new-path)
                  (add-text-properties 0 1 (list :path (substring-no-properties new-path) :type (if (equal type "tree") "directory" "file")) new-path)))
             (new-files (remove nil (cl-remove-duplicates (append (list (and (stringp new-path) new-path)) files) :test #'equal))))
        (add-text-properties 0 1 (list :files new-files) commit)
        (add-text-properties 0 1 (list :diff-buffer nil) commit)
        (save-excursion
          (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                    (goto-char (car-safe (car-safe regions)))))
         (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
       (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
       (when (listp new-files)
         (insert (propertize "\n\n" :consult-gh-commit-instructions t))
         (insert (propertize "# Files to delete:" :consult-gh-commit-instructions t))
         (mapc (lambda (f)
                 (unless (member f files)
                 (consult-gh--commit-delete-make-diff-buffer repo f ref))
                   (insert (propertize (format "\n# - delete %s" f) :consult-gh-commit-instructions t)))
                 new-files))))))

(defun consult-gh--delete-commit-remove-files (&optional commit)
  "Remove files from the files to upload in COMMIT."
  (if consult-gh-topics-edit-mode
      (let* ((files (get-text-property 0 :files commit))
             (file (consult--read files
                                  :prompt "Remove Files: "))
             (new-files (remove file files)))
          (add-text-properties 0 1 (list :files new-files) commit)
          (add-text-properties 0 1 (list :diff-buffer nil) commit)
       (save-excursion
         (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                    (goto-char (car-safe (car-safe regions)))))
         (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
       (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
       (when (listp new-files)
         (insert (propertize "\n\n" :consult-gh-commit-instructions t))
         (insert (propertize "# Files to delete:" :consult-gh-commit-instructions t))
         (mapc (lambda (f)
                   (insert (propertize (format "\n# - delete %s" f) :consult-gh-commit-instructions t)))
                 new-files))))))

(defun consult-gh--delete-commit-by-pullrequest (&optional repo files commit-message ref committer-info author-info)
"Submit delete commit by creating a pull request.

This is used when REF branch is protected and does not allow direct
commits.  A new branch is created and a pull request is made to merge
the new branch to REF.

Description of Arguments:
  REPO           a string; full name of repository
  FILES          a list strings; each string must be a path in REPO
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of the protected branch ref for commit
  COMMITTER-INFO a plist; committer info plist
                 \(:name NAME :email EMAIL\)
  AUTHOR-INFO    a plist; author info plist
                 \(:name NAME :email EMAIL\)"

  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (initial (or (consult-gh--branch-create-suggest-name repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
         (existing-branches (consult-gh--repo-get-branches-list repo))
         (new-ref (consult--read (list initial)
                                         :prompt "Name of the new branch: "
                                         :sort nil))
         (tries (if (stringp new-ref) 1))
         (new-ref (if (and (stringp new-ref)
                               (member new-ref existing-branches))
                      (while (< tries 3)
                        (cl-incf tries)
                        (consult--read (list initial)
                                         :prompt (format "A branch with that name already exists. Pick a different name (attempt %s/3): " tries)
                                         :sort nil))
                        new-ref))
         (_ (if (not new-ref) (user-error "Did not get a valid branch name")))
         (ref (or ref (consult-gh--read-branch repo nil "Select reference branch for starting point: " t nil)))
         (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                  (and (stringp ref)
                       (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
         (args (list "api" "-X" "POST")))
    (when (and repo new-ref sha)
      (setq args (append args (list (format "/repos/%s/git/refs" repo)
                                    "-f" (format "ref=refs/heads/%s" new-ref)
                                    "-f" (format "sha=%s" sha))))

      (consult-gh--make-process (format "consult-gh-create-branch-%s" repo)
                                :when-done (lambda (_event _str)
                                              (and
                                               (consult-gh--delete-commit-submit repo files commit-message new-ref committer-info author-info)
                                               (message "Creating a Pull Request...")
                                               (consult-gh-pr-create repo (format "Delete Files") nil ref repo new-ref)))
                                      :cmd-args args))))

(defun consult-gh--delete-commit-single-file (repo file commit-message &optional ref committer-info author-info)
  "Submit a delete file commit.

Description of Arguments:
  REPO           a string; full name of repository
  FILE           a string; path of file in REPO to delete
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"
  (pcase-let* ((committer-name (when (hash-table-p committer-info) (gethash :name committer-info)))
               (committer-email (when (hash-table-p committer-info) (gethash :email committer-info)))
               (author-name (when (hash-table-p author-info) (gethash :name author-info)))
               (author-email (when (hash-table-p author-info) (gethash :email author-info)))
               (url (format "/repos/%s/contents/%s" repo file))
               (url-ref (if ref (concat url (format "?ref=%s" ref)) url))
               (api-response (consult-gh--api-get-json url-ref)))

      (when (eq (car api-response) 0)
        (let* ((resp (consult-gh--json-to-hashtable (cadr api-response) (list :path :sha)))
               (shas (cond
                      ((hash-table-p resp)
                       (list resp))
                      ((listp resp)
                       resp))))

          (when (and repo shas)
            (cl-loop for item in shas
                     collect
                     (let* ((item-file (gethash :path item))
                            (item-sha (gethash :sha item))
                            (commit-message (or (and (stringp commit-message)
                                                     (not (string-empty-p (string-trim commit-message)))
                                                     (concat commit-message
                                                             (format "\n delete %s\n"
                                                                     item-file)))
                                                (and (stringp item-file)
                                                     (format "Delete %s\n" item-file))
                                                (consult--read nil
                                                               :prompt "Commit Message: "
                                                               :sort nil)))
                            (args (list "api"
                                        "-H" "Accept: application/vnd.github+json"
                                        "--method" "DELETE"
                                        (format "/repos/%s/contents/%s" repo item-file)
                                        "-f" (format "message=%s" commit-message)
                                        "-f" (format "sha=%s" item-sha))))
                       (when ref (setq args (append args (list "-f" (format "branch=%s" ref)))))
                       (when (and committer-name committer-email)
                         (setq args (append args (list "-f" (format "committer[name]=%s" committer-name)
                                                       "-f" (format "committer[email]=%s" committer-email)))))
                       (when (and author-name author-email)
                         (setq args (append args (list "-f" (format "author[name]=%s" author-name)
                                                       "-f" (format "author[email]=%s" author-email)))))
                       (apply #'consult-gh--command-to-string args))))))))

(defun consult-gh--delete-commit-submit (repo files commit-message &optional ref committer-info author-info)
  "Submit a delete files commit.

Description of Arguments:
  REPO           a string; full name of repository
  FILES          a list of strings; list of paths for files to delete
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((canwrite (consult-gh--user-canwrite repo))
         (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (_  (unless canwrite
               (user-error "Current user, %s, does not have permissions to create a branch in %s" user repo)))
         (commit-message (or (and (stringp commit-message)
                                  (not (string-empty-p (string-trim commit-message)))
                                  commit-message)
                             "Delete files\n\n"
                             (consult--read nil
                                            :prompt "Commit Message: "
                                            :sort nil)))
         (protected (when ref (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/branches/%s" repo ref)) :protected))))

    (when (and commit-message
               (stringp commit-message)
               (not (string-empty-p commit-message)))
      (consult-gh-commit-save-message commit-message))

    (cond
     ((equal protected t)
      (and (y-or-n-p "The branch you are trying to edit is protected.  Do you want to submit a pull request? ")
           (consult-gh--delete-commit-by-pullrequest repo files commit-message ref committer-info author-info)))
     (t
      (when (and canwrite repo files commit-message)
        (let* ((confirm nil))
          (if (stringp files)
              (setq files (list files)))
          (cl-loop for path in files
                   collect
                   (progn
                     (unless (equal confirm "all")
                       (setq confirm (read-answer (format "Delete %s on GitHub? "
                                                          (propertize path 'face 'consult-gh-warning))
                                                  '(("yes"  ?y "Delete this file")
                                                    ("no"   ?n "skip this file")
                                                    ("all"  ?! "Delete all remaining files without more questions")
                                                    ("help" ?h "show help")
                                                    ("quit" ?q "exit")))))
                     (cond
                      ((or (equal confirm "yes")
                           (equal confirm "all"))
                       (and
                        (consult-gh--delete-commit-single-file repo path commit-message ref committer-info author-info)
                        (message "%s deleted!" (propertize path 'face 'consult-gh-warning))))
                      ((equal confirm "no")
                       (message "Skipped %s" (propertize path 'face 'consult-gh-error)))
                      (t (message "%s" (propertize "Canceled!" 'face 'consult-gh-error))))))))))))

(defun consult-gh--delete-commit-presubmit (&optional commit)
  "Prepare delete COMMIT to submit.

COMMIT is a string with properties that identify a commit.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--delete-commit'."
  (if consult-gh-commit-message-mode
      (let* ((commit (or commit
                       (and (stringp consult-gh--topic)
                            (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                            consult-gh--topic)))
             (repo (get-text-property 0 :repo commit))
             (ref (or (get-text-property 0 :commit-ref commit)
                     (get-text-property 0 :ref commit)))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (user-info (consult-gh--get-user-info user))
             (committer-info (or (get-text-property 0 :committer-info commit) user-info))
             (committer (or (and (hash-table-p committer-info)
                                 (or (gethash :name committer-info)
                                     (gethash :email committer-info))
                                 (concat (gethash :name committer-info) " - " (gethash :email committer-info)))
                            user))
             (author-info  (or (get-text-property 0 :author-info commit)
                               user-info))
             (author (or (and (hash-table-p author-info)
                              (or (gethash :name author-info)
                                (gethash :email author-info))
                          (concat (gethash :name author-info) " - " (gethash :email author-info)))
                           user))
             (nextsteps (append (list (cons "Submit" :submit))
                                (list (cons "Add Files" :add))
                                (list (cons "Remove Files" :remove))
                                (list (cons (format "Change Branch (current: %s)" ref) :ref))
                                (list (cons (format "Change Committer (current: %s)" committer) :committer))
                                (list (cons (format "Change Author (current: %s)" author) :author))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil))

             (buffer (current-buffer)))
          (pcase next
            (':cancel)
            (':submit
             (let* ((ref (or (get-text-property 0 :commit-ref consult-gh--topic)
                                (get-text-property 0 :ref consult-gh--topic)))
                    (files (get-text-property 0 :files commit))
                    (commit-message (consult-gh--commit-get-buffer-message)))
               (and (consult-gh--delete-commit-submit repo files commit-message ref committer-info author-info)
                    (message "Commit Submitted!")
                    (with-current-buffer buffer
                    (funcall consult-gh-quit-window-func t)))))
            (':add (consult-gh--delete-commit-add-files repo ref commit))
            (':remove (consult-gh--delete-commit-remove-files commit))
            (':committer (consult-gh--commit-change-committer)
                         (consult-gh--delete-commit-presubmit))
            (':author (consult-gh--commit-change-author)
                      (consult-gh--delete-commit-presubmit))
            (':ref (consult-gh--commit-change-ref)
                   (consult-gh--delete-commit-presubmit))))
    (message "Not a Github commit buffer!")))

(defun consult-gh--commit-upload-make-diff-buffer (repo file path &optional ref)
  "Make a diff buffer for a commit to upload FILE in REPO at PATH.

REF is the name of a branch or tag name."
  (let* ((target "upload")
         (local-path (get-text-property 0 :local-path file))
         (remote-path (get-text-property 0 :remote-path file))
         (type (get-text-property 0 :type file))
         (ref (or ref "HEAD"))
         (diff-buff (get-buffer-create (format " *consult-gh-diff-%s-%s-%s:%s*" target repo ref remote-path))))
    (with-current-buffer diff-buff
      (let ((inhibit-read-only t))
        (erase-buffer))
      (add-text-properties 0 1 (list :diff-done nil) file))
    (add-text-properties 0 1 (list :diff-buffer diff-buff) file)
    (pcase type
      ("directory"
       (let* ((files-list (directory-files-recursively local-path ".*")))
         (mapc (lambda (f)
                 (let* ((f-name (file-name-nondirectory f))
                        (f-relative-path (string-remove-prefix (file-name-directory (file-truename local-path)) (file-truename f)))

                        (f-parent-path (concat
                                        (if path (file-name-as-directory path))
                                        (file-name-as-directory (file-name-directory f-relative-path))))
                        (f-path (format "%s%s" (if (stringp f-parent-path) (file-name-as-directory f-parent-path) "") f-name))

                        (url (format "/repos/%s/contents/%s" repo f-path))
                        (url-ref (if ref (concat url (format "?ref=%s" ref)) url)))
                   (when (stringp f-path)
                     (add-text-properties 0 1 (list :diff-done nil) file)
                     (consult-gh--make-process (format "consult-gh-diff-upload:%s/%s/%s" repo ref f-path)
                                               :on-error
                                               (lambda (_event _str)
                                                 (with-temp-buffer
                                                   (when (file-exists-p f)
                                                     (insert-file-contents f)
                                                     (set-buffer-file-coding-system 'raw-text)
                                                     (set-buffer-multibyte t))
                                                   (let ((new-buffer (current-buffer)))
                                                     (with-temp-buffer
                                                       (insert "")
                                                       (set-buffer-file-coding-system 'raw-text)
                                                       (set-buffer-multibyte t)
                                                       (let ((old-buffer (current-buffer)))

                                                         (with-temp-buffer
                                                           (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                                           (let ((diff-str (buffer-string)))
                                                             (when (stringp diff-str)
                                                               (with-current-buffer diff-buff
                                                                 (goto-char (point-max))
                                                                 (insert (concat (format "Uploading %s:\n\n" f-path)
                                                                         diff-str
                                                                         "\n\n"))
                                                                 (add-text-properties 0 1 (list :diff-done t) file))))))))))
                                               :when-done
                                               (lambda (_event str)
                                                 (let* ((content (when (stringp str) (consult-gh--json-to-hashtable str :content)))
                                                        (old-content (when (stringp content)
                                                                       (base64-decode-string content))))
                                                   (with-temp-buffer
                                                     (when (file-exists-p f)
                                                       (insert-file-contents f)
                                                       (set-buffer-file-coding-system 'raw-text)
                                                       (set-buffer-multibyte t))
                                                     (let ((new-buffer (current-buffer)))
                                                       (with-temp-buffer
                                                         (insert (or old-content ""))
                                                         (set-buffer-file-coding-system 'raw-text)
                                                         (set-buffer-multibyte t)
                                                         (let ((old-buffer (current-buffer)))
                                                           (with-temp-buffer
                                                             (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                                             (let ((diff-str (buffer-string)))
                                                               (when (stringp diff-str)
                                                                 (with-current-buffer diff-buff
                                                                   (goto-char (point-max))
                                                                   (insert (concat (format "Uploading %s:\n\n" f-path)
                                                                           diff-str
                                                                           "\n\n"))
                                                                   (add-text-properties 0 1 (list :diff-done t) file)))))))))))
                                               :cmd-args (list "api" "-H" "Accept: application/vnd.github+json" "--paginate" url-ref)))))
               files-list)))
      ("file"
       (let* ((url (format "/repos/%s/contents/%s" repo remote-path))
              (url-ref (if ref (concat url (format "?ref=%s" ref)) url)))
         (consult-gh--make-process (format "consult-gh-diff-upload:%s/%s/%s" repo ref remote-path)
                                   :on-error
                                   (lambda (_event _str)
                                     (with-temp-buffer
                                       (when (file-exists-p local-path)
                                         (insert-file-contents local-path)
                                         (set-buffer-file-coding-system 'raw-text)
                                         (set-buffer-multibyte t))
                                       (let ((new-buffer (current-buffer)))
                                         (with-temp-buffer
                                           (insert "")
                                           (set-buffer-file-coding-system 'raw-text)
                                           (set-buffer-multibyte t)
                                           (let ((old-buffer (current-buffer)))
                                             (with-temp-buffer
                                               (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                               (let ((diff-str (buffer-string)))
                                                 (when (stringp diff-str)
                                                   (with-current-buffer diff-buff
                                                     (goto-char (point-max))
                                                     (insert (concat (format "Uploading %s:\n\n" remote-path)
                                                             diff-str
                                                             "\n\n"))
                                                     (add-text-properties 0 1 (list :diff-done t) file))))))))))
                                   :when-done
                                   (lambda (_event str)
                                     (let* ((content (when (stringp str) (consult-gh--json-to-hashtable str :content)))
                                            (old-content (when (stringp content)
                                                           (base64-decode-string content))))
                                       (with-temp-buffer
                                         (when (file-exists-p local-path)
                                           (insert-file-contents local-path)
                                           (set-buffer-file-coding-system 'raw-text)
                                           (set-buffer-multibyte t))
                                         (let ((new-buffer (current-buffer)))
                                           (with-temp-buffer
                                             (insert (or old-content ""))
                                             (set-buffer-file-coding-system 'raw-text)
                                             (set-buffer-multibyte t)
                                             (let ((old-buffer (current-buffer)))
                                               (with-temp-buffer
                                                 (diff-no-select old-buffer new-buffer nil t (current-buffer))
                                                 (let ((diff-str (buffer-string)))
                                                   (when (stringp diff-str)
                                                     (with-current-buffer diff-buff
                                                       (goto-char (point-max))
                                                       (insert (concat (format "Uploading %s:\n\n" remote-path)
                                                               diff-str
                                                               "\n\n"))
                                                       (add-text-properties 0 1 (list :diff-done t) file)))))))))))
                                   :cmd-args (list "api" "-H" "Accept: application/vnd.github+json" "--paginate" url-ref))))
      (_
       (with-current-buffer diff-buff
         (goto-char (point-max))
          (insert (concat (format "Uploading %s:\n\n" (get-text-property 0 :remote-path file))
                  "\n\n")))))))

(defun consult-gh--upload-commit (files repo path ref &optional commit-message)
  "Create a commit to upload FILES at PATH in REPO and REF.

It opens a buffer to enter the commit message for uploading FILES.
FILES must be a list of local file paths.

REF is the name of a branch in repo.

If COMMIT-MESSAGE is non-nil, it is inserted in the buffer as initial
message."

   (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (committer-info (consult-gh--get-user-info user))
          (author-info (consult-gh--get-user-info user))
          (buffer (format "*consult-gh-files-upload-commit-message: %s/%s/%s" repo ref path))
          (newtopic (format "%s/%s/%s" ref repo path))
          (type "commit message"))

     (add-text-properties 0 1 (list :isComment nil :type type :new t :number nil :committer-info committer-info :author-info author-info :commit-ref ref :content nil :commit-target "upload" :repo repo :path path :ref ref :files files) newtopic)

     ;; insert commit message
     (consult-gh-topics--get-buffer-create buffer "commit message" newtopic)
     (with-current-buffer buffer
       (consult-gh-commit-message-mode +1)
       (save-excursion
         (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                    (goto-char (car-safe (car-safe regions)))))
         (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
       (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
       (when (listp files)
         (insert (propertize "\n\n" :consult-gh-commit-instructions t))
         (insert (propertize (concat "# Files to upload:" (if path (format "at %s" path))) :consult-gh-commit-instructions t))
         (mapc (lambda (f)
                 (add-text-properties 0 1 (list :local-path (substring-no-properties f) :remote-path (concat (file-name-as-directory path) (file-name-nondirectory f))
                                                :type (if (file-directory-p f) "directory" "file"))
                                      f)
                 (consult-gh--commit-upload-make-diff-buffer repo f path ref)
                 (insert (propertize (format "\n# - upload %s" f) :consult-gh-commit-instructions t)))
                 files)))
       (goto-char (point-min))
       (when (stringp commit-message)
         (insert commit-message)
         (consult-gh-commit-save-message)))
     (funcall consult-gh-pop-to-buffer-func buffer)))

(defun consult-gh--upload-commit-add-files (&optional commit)
  "Add files to the files to upload in COMMIT."
  (if consult-gh-topics-edit-mode
      (let* ((files (get-text-property 0 :files commit))
             (repo (get-text-property 0 :repo commit))
             (ref (get-text-property 0 :ref commit))
             (path (get-text-property 0 :path commit))
             (new-file (consult-gh--read-local-file nil "Add Files: " default-directory t))
             (new-files (remove nil (cl-remove-duplicates (append (list (and (stringp new-file) (string-remove-suffix "/" new-file))) files) :test #'equal))))
        (add-text-properties 0 1 (list :files new-files)
                               commit)
(save-excursion
         (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                    (goto-char (car-safe (car-safe regions)))))
         (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
       (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
       (when (listp new-files)
         (insert (propertize "\n\n" :consult-gh-commit-instructions t))
         (insert (propertize "# Files to upload:" :consult-gh-commit-instructions t))
         (mapc (lambda (f)
                 (add-text-properties 0 1 (list :local-path (substring-no-properties f) :remote-path (concat (file-name-as-directory path) (file-name-nondirectory f))
                                                :type (if (file-directory-p f) "directory" "file"))
                                      f)
                 (consult-gh--commit-upload-make-diff-buffer repo f path ref)
                 (insert (propertize (format "\n# - upload %s" f) :consult-gh-commit-instructions t)))
                 new-files))))))

(defun consult-gh--upload-commit-remove-files (&optional commit)
  "Remove files from the files to upload in COMMIT."
  (if consult-gh-topics-edit-mode
      (let* ((files (get-text-property 0 :files commit))
             (new-files (remove (consult--read files
                                               :prompt "Remove Files: "
                                               :lookup #'consult--lookup-member) files)))
          (add-text-properties 0 1 (list :files new-files)
                               commit)
          (add-text-properties 0 1 (list :diff-buffer nil) commit)
       (save-excursion
         (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                    (goto-char (car-safe (car-safe regions)))))
         (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
       (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
       (when (listp files)
         (insert (propertize "\n\n" :consult-gh-commit-instructions t))
         (insert (propertize "# Files to upload:" :consult-gh-commit-instructions t))
         (mapc (lambda (f)
                   (insert (propertize (format "\n# - upload %s" f) :consult-gh-commit-instructions t)))
                 new-files))))))

(defun consult-gh--upload-commit-by-pullrequest (files repo path &optional commit-message ref committer-info author-info)
"Submit upload commit by creating a pull request.

This is used when REF is protected and does not allow direct
commits.  A new branch is created and a pull request is made to merge
the new branch to REF.

Description of Arguments:
  FILES          a list os strings; list of local file paths
  REPO           a string; full name of repository
  PATH           a string; path of file to change
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of the protected branch ref for commit
  COMMITTER-INFO a plist; committer info plist
                 \(:name NAME :email EMAIL\)
  AUTHOR-INFO    a plist; author info plist
                 \(:name NAME :email EMAIL\)
  SHA            a string; current sha of the file at PATH in REF"

  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (initial (or (consult-gh--branch-create-suggest-name repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
         (existing-branches (consult-gh--repo-get-branches-list repo))
         (new-ref (consult--read (list initial)
                                         :prompt "Name of the new branch: "
                                         :sort nil))
         (tries (if (stringp new-ref) 1))
         (new-ref (if (and (stringp new-ref)
                               (member new-ref existing-branches))
                      (while (< tries 3)
                        (cl-incf tries)
                        (consult--read (list initial)
                                         :prompt (format "A branch with that name already exists. Pick a different name (attempt %s/3): " tries)
                                         :sort nil))
                        new-ref))
         (_ (if (not new-ref) (user-error "Did not get a valid branch name")))
         (ref (or ref (consult-gh--read-branch repo nil "Select reference branch for starting point: " t nil)))
         (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                  (and (stringp ref)
                       (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
         (args (list "api" "-X" "POST")))
    (when (and repo new-ref sha)
      (setq args (append args (list (format "/repos/%s/git/refs" repo)
                                    "-f" (format "ref=refs/heads/%s" new-ref)
                                    "-f" (format "sha=%s" sha))))

      (consult-gh--make-process (format "consult-gh-create-branch-%s" repo)
                                :when-done (lambda (_event _str)
                                               (and
                                                (consult-gh--upload-commit-submit files repo path commit-message new-ref committer-info author-info)
                                                (message "Creating a Pull Request...")
                                               (consult-gh-pr-create repo (format "Upload files to %s" path) nil ref repo new-ref)))
                                :cmd-args args))))

(defun consult-gh--upload-commit-single-file (file parent-path repo &optional commit-message ref committer-info author-info)
  "Submit a commit to upload a single file.

Description of Arguments:
  FILE           a list os strings; list of local file paths
  REPO           a string; full name of repository
  PARENT-PATH    a string; path of the parent directory in repo for file
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((file-name (file-name-nondirectory file))
         (content (ignore-errors (with-temp-buffer
                                   (insert-file-contents file)
                                   (set-buffer-file-coding-system 'raw-text)
                                   (set-buffer-multibyte t)
                                   (base64-encode-string (substring-no-properties (encode-coding-string (buffer-substring-no-properties (point-min) (point-max)) 'utf-8))))))
         (content (or content ""))
         (file-path (format "%s%s" (if (stringp parent-path) (file-name-as-directory parent-path) "") file-name))
         (committer-name (when (hash-table-p committer-info) (gethash :name committer-info)))
         (committer-email (when (hash-table-p committer-info) (gethash :email committer-info)))
         (author-name (when (hash-table-p author-info) (gethash :name author-info)))
         (author-email (when (hash-table-p author-info) (gethash :email author-info)))
         (url (format "/repos/%s/contents/%s" repo file-path))
         (url-ref (if ref (concat url (format "?ref=%s" ref)) url))
         (api-response (consult-gh--api-get-json url-ref))
         (file-sha (if (eq (car api-response) 0)
                       (consult-gh--json-to-hashtable (cadr api-response) :sha)))
         (args (list "api"
                     "-H" "Accept: application/vnd.github+json"
                     "--method" "PUT"
                     url
                     "-f" (format "message=%s" commit-message)
                     "-f" (format "content=%s" content))))
    (when ref (setq args (append args (list "-f" (format "branch=%s" ref)))))
    (when (and committer-name committer-email)
      (setq args (append args (list "-f" (format "committer[name]=%s" committer-name)
                                    "-f" (format "committer[email]=%s" committer-email)))))
    (when (and author-name author-email)
      (setq args (append args (list "-f" (format "author[name]=%s" author-name)
                                    "-f" (format "author[email]=%s" author-email)))))

    (if file-sha
        (if (y-or-n-p (format "File %s exists.  Do you want to replace it with the new file? " file-path))
            (progn (setq args (append args (list "-f" (format "sha=%s" file-sha))))
                   (apply #'consult-gh--command-to-string args)
                   (message "File %s uploaded!" (propertize file-name 'face 'cponsult-gh-success)))
          (message "File %s Skipped!" (propertize file-name 'face 'consult-gh-error)))
    (progn
      (and
       (apply #'consult-gh--command-to-string args)
       (message "File %s uploaded!" (propertize file-name 'face 'consult-gh-success)))))))

(defun consult-gh--upload-commit-submit (files repo &optional path commit-message ref committer-info author-info)
  "Submit a commit to upload files.

Description of Arguments:
  FILES          a list os strings; list of local file paths
  REPO           a string; full name of repository
  PATH           a string; path of file to delete
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (pcase-let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
               (canwrite (consult-gh--user-canwrite repo))
               (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
               (_  (unless canwrite
                     (user-error "Current user, %s, does not have permissions to create a branch in %s" user repo)))
               (commit-message (or (and (stringp commit-message)
                                        (not (string-empty-p (string-trim commit-message)))
                                        commit-message)
                                   (and (listp files)
                                        (format "Upload Files\n\n%s\n" (mapconcat (lambda (item) (format " - %s" (file-name-nondirectory item))) files "\n")))
                                   (consult--read nil
                                                  :prompt "Commit Message: "
                                                  :sort nil)))
               (protected (when ref (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/branches/%s" repo ref)) :protected))))

    (when (and commit-message
               (stringp commit-message)
               (not (string-empty-p commit-message)))
      (consult-gh-commit-save-message commit-message))

    (cond
     ((equal protected t)
      (and (y-or-n-p "The branch you are trying to edit is protected.  Do you want to submit a pull request? ")
           (consult-gh--upload-commit-by-pullrequest files repo path commit-message ref committer-info author-info)))
     (t
      (when (and canwrite repo (listp files) commit-message)
        (let* ((confirm))
          (cl-loop for file in files
                   collect
                   (progn
                     (unless (equal confirm "all")
                     (setq confirm (read-answer (format "This will upload %s on GitHub.  Are you sure you want to continue?" (propertize file 'face 'consult-gh-warning))
                                                '(("yes"  ?y "upload the file/directory")
                                                  ("no"   ?n "skip to the next file/directory")
                                                  ("all"  ?! "accept all remaining without more questions")
                                                  ("help" ?h "show help")
                                                  ("quit" ?q "exit")))))
                   (cond
                    ((or (equal confirm "yes")
                             (equal confirm "all"))

                     (if (file-directory-p file)
                         (mapcar (lambda (f)
                                   (let* ((relative-path (string-remove-prefix (file-name-directory (file-truename file)) (file-truename f)))
                                          (parent-path (concat (if path (file-name-as-directory path))
                                                               (file-name-as-directory (file-name-directory relative-path)))))
                                     (consult-gh--upload-commit-single-file f parent-path repo commit-message ref committer-info author-info)))
                                 (directory-files-recursively file ".*"))
                       (let* ((parent-path (if path (file-name-as-directory path))))
                         (consult-gh--upload-commit-single-file file parent-path repo commit-message ref committer-info author-info))))
                    ((equal confirm "no")
                     (message "Skipped %s" (propertize file 'face 'consult-gh-error)))
                    (t (message "Canceled!")))))))))))

(defun consult-gh--upload-commit-presubmit (&optional commit)
  "Prepare upload COMMIT to submit.

COMMIT is a string with properties that identify a commit.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--upload-commit'."
  (if consult-gh-commit-message-mode
      (let* ((commit (or commit
                       (and (stringp consult-gh--topic)
                            (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                            consult-gh--topic)))
             (repo (get-text-property 0 :repo commit))
             (ref (or (get-text-property 0 :commit-ref commit)
                     (get-text-property 0 :ref commit)))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (user-info (consult-gh--get-user-info user))
             (committer-info (or (get-text-property 0 :committer-info commit) user-info))
             (committer (or (and (hash-table-p committer-info)
                                 (or (gethash :name committer-info)
                                     (gethash :email committer-info))
                                 (concat (gethash :name committer-info) " - " (gethash :email committer-info)))
                            user))
             (author-info  (or (get-text-property 0 :author-info commit)
                               user-info))
             (author (or (and (hash-table-p author-info)
                              (or (gethash :name author-info)
                                (gethash :email author-info))
                          (concat (gethash :name author-info) " - " (gethash :email author-info)))
                           user))
             (nextsteps (append (list (cons "Submit" :submit))
                                (list (cons "Add Files" :add))
                                (list (cons "Remove Files" :remove))
                                (list (cons (format "Change Branch (current: %s)" ref) :ref))
                                (list (cons (format "Change Committer (current: %s)" committer) :committer))
                                (list (cons (format "Change Author (current: %s)" author) :author))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil))

             (buffer (current-buffer)))
          (pcase next
            (':cancel)
            (':submit
             (let* ((ref (or (get-text-property 0 :commit-ref consult-gh--topic)
                                (get-text-property 0 :ref consult-gh--topic)))
                    (path (get-text-property 0 :path commit))
                    (files (get-text-property 0 :files commit))
                    (commit-message (consult-gh--commit-get-buffer-message)))
               (and (consult-gh--upload-commit-submit files repo path commit-message ref committer-info author-info)
                    (message "Commit Submitted!")
                    (with-current-buffer buffer
                      (when-let* ((remove-topics (remove nil (mapcar (lambda (topic) (when (stringp topic)
                                              (let* ((topic-repo (get-text-property 0 :repo topic))
                                                     (topic-ref (get-text-property 0 :ref topic))
                                                     (topic-path (get-text-property 0 :path topic)))
                                                (if (and (equal repo topic-repo)
                                                         (equal ref topic-ref)
                                                         (equal path topic-path))
                                                    topic))))
                            consult-gh--upload-targets))))
                        (setq consult-gh--upload-targets (seq-difference consult-gh--upload-targets remove-topics)))
                    (funcall consult-gh-quit-window-func t)))))
            (':add (consult-gh--upload-commit-add-files commit))
            (':remove (consult-gh--upload-commit-remove-files commit))
            (':committer (consult-gh--commit-change-committer)
                         (consult-gh--upload-commit-presubmit))
            (':author (consult-gh--commit-change-author)
                      (consult-gh--upload-commit-presubmit))
            (':ref (consult-gh--commit-change-ref)
                      (consult-gh--upload-commit-presubmit))))
    (message "Not a Github commit buffer!")))

(defun consult-gh--commit-rename-make-diff-buffer (repo file &optional ref)
  "Make a diff buffer for a commit to rename a FILE in REPO.

REF is the name of a branch or tag name."
  (let* ((old-path (get-text-property 0 :old-path file))
         (new-path (get-text-property 0 :new-path file))
         (viewbuffer (or (get-text-property 0 :view-buffer file)
                         (consult-gh--files-view repo old-path nil t nil nil ref)))
         (target "rename")
         (content  (or (get-text-property 0 :content file)
                       (and (bufferp viewbuffer)
                            (buffer-live-p viewbuffer)
                            (with-current-buffer viewbuffer
                              (save-restriction
                                (widen)
                                (buffer-substring-no-properties (point-min) (point-max)))))))
         (base64-content (and (stringp content)
                              (base64-encode-string (substring-no-properties (encode-coding-string content 'utf-8)))))
         (diff-buffer (with-current-buffer (get-buffer-create (format " *consult-gh-diff-%s-%s-%s:%s-%s*" target repo ref old-path new-path))
                        (let ((inhibit-read-only t))
                          (erase-buffer)
                          (insert (format "diff --git a/%s b/%s\nsimilarity index 100%%\nrename from %s\nrename to %s" old-path new-path old-path new-path))
                          (current-buffer)))))
    (add-text-properties 0 1 (list :base64-content base64-content :diff-buffer diff-buffer) file)))

(defun consult-gh--rename-commit (files repo ref &optional commit-message)
  "Create a commit to rename FILES in REPO and REF.

REPO is full name of a GitHub repository.  REF is the name of a branch
or tag name.  FILES is a list of strings with properties that identify
a file.  For an example, see the buffer-local variable
`consult-gh--topic' in the buffer generated by
`consult-gh--files-view'.  It defaults to
`consult-gh--topic' in the current buffer.

It opens a buffer to enter the commit message for renaming FILES.
If COMMIT-MESSAGE is non-nil, it is inserted in the buffer as initial
message."
  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (committer-info (consult-gh--get-user-info user))
         (author-info (consult-gh--get-user-info user))
         (buffer (format "*consult-gh-file-rename-commit-message: %s/%s" repo ref))
         (newtopic (format "%s/%s" repo ref))
         (type "commit message"))

    (add-text-properties 0 1 (list :isComment nil :type type :new t :number nil :committer-info committer-info :author-info author-info :commit-ref ref :commit-target "rename" :repo repo :ref ref :files files) newtopic)

    ;; insert commit message
    (consult-gh-topics--get-buffer-create buffer "commit message" newtopic)
    (with-current-buffer buffer
      (consult-gh-commit-message-mode +1)
      (save-excursion
        (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions)))
          (goto-char (or (car-safe (car-safe regions)) (point-min))))
        (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
        (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
        (when (listp files)
          (insert (propertize "\n\n" :consult-gh-commit-instructions t))
          (insert (propertize "# Files to Rename:" :consult-gh-commit-instructions t))
          (mapc (lambda (f)
                  (let* ((old-path (get-text-property 0 :old-path f))
                         (new-path (get-text-property 0 :new-path f)))
                  (consult-gh--commit-rename-make-diff-buffer repo f ref)
                  (add-text-properties 0 1 (list :commit-buffer (get-buffer buffer)) f)
                  (unless (equal old-path new-path)
                    (insert (propertize (format "\n# - rename %s -> %s" old-path new-path) :consult-gh-commit-instructions t)))))
                files))
        (goto-char (point-min))
        (when (stringp commit-message)
          (insert commit-message)
          (consult-gh-commit-save-message))
        (funcall consult-gh-pop-to-buffer-func buffer)))))

(defun consult-gh--rename-commit-by-pullrequest (files repo &optional commit-message ref committer-info author-info)
  "Submit rename commit by creating a pull request.

This is used when REF is protected and does not allow direct
commits.  A new branch is created and a pull request is made to merge
the new branch to REF.

Description of Arguments:
  FILES          a list of propertized strings; each string contains
                 information \(e.g. path, content, ...) for 1 file.
  REPO           a string; full name of repository
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (initial (or (consult-gh--branch-create-suggest-name repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
         (existing-branches (consult-gh--repo-get-branches-list repo))
         (new-ref (consult--read (list initial)
                                 :prompt "Name of the new branch: "
                                 :sort nil))
         (tries (if (stringp new-ref) 1))
         (new-ref (if (and (stringp new-ref)
                           (member new-ref existing-branches))
                      (while (< tries 3)
                        (cl-incf tries)
                        (consult--read (list initial)
                                       :prompt (format "A branch with that name already exists. Pick a different name (attempt %s/3): " tries)
                                       :sort nil))
                    new-ref))
         (_ (if (not new-ref) (user-error "Did not get a valid branch name")))
         (ref (or ref (consult-gh--read-branch repo nil "Select reference branch for starting point: " t nil)))
         (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                  (and (stringp ref)
                       (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
         (args (list "api" "-X" "POST")))
    (when (and repo new-ref sha)
      (setq args (append args (list (format "/repos/%s/git/refs" repo)
                                    "-f" (format "ref=refs/heads/%s" new-ref)
                                    "-f" (format "sha=%s" sha))))

      (consult-gh--make-process (format "consult-gh-create-branch-%s" repo)
                                :when-done (lambda (_event _str)
                                             (and
                                              (consult-gh--rename-commit-submit files repo commit-message new-ref committer-info author-info)
                                              (message "Creating a Pull Request...")
                                              (consult-gh-pr-create repo "Update Files" nil ref repo new-ref)))
                                :cmd-args args))))

(defun consult-gh--rename-commit-by-fork (files repo &optional commit-message ref committer-info author-info)
  "Submit rename commit by forking REPO and creating a pull request.

This is used when user does not have write access in REPO.  The REPO is
first forked, then the changes are committed in the fork and pull
request is made to merge the new branch in the fork back to REF in
REPO.

Description of Arguments:
  FILES          a list of propertized strings; each string contains
                 information \(e.g. path, content, ...) for 1 file.
  REPO           a string; full name of repository to fork
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (name (consult-gh--get-package repo))
         (new-repo (concat user "/" name))
         (existing (consult-gh--command-to-string "repo" "view" new-repo "--json" "\"url\"")))
    (cond
     ((not existing)
      (consult-gh--make-process (format "consult-gh-fork-%s" repo)
                              :when-done (lambda (_event _str)
                                            (and
                                             (message "repo %s was forked to %s"
                                                      (propertize repo 'face 'font-lock-keyword-face)
                                                      (propertize new-repo 'face 'font-lock-warning-face))
                                             (consult-gh--rename-commit-submit files new-repo commit-message ref committer-info author-info)
                                             (message "Creating a Pull Request...")
                                             (consult-gh-pr-create repo "Create/Update Files" nil ref new-repo ref)))
                              :cmd-args (list "repo" "fork" (format "%s" repo) "--fork-name" name)))
     (t
      (message "Forked repo already exists. Making a new branch...")
      (let* ((initial (or (consult-gh--branch-create-suggest-name new-repo user)
                      (format "%s-patch-%s"
                              user
                              (format-time-string "%Y-%m-%d-%H%M%S%Z" (current-time)))))
             (existing-branches (consult-gh--repo-get-branches-list new-repo))
             (new-ref (consult--read (list initial)
                                         :prompt "Name of the new branch: "
                                         :sort nil))
             (tries (if (stringp new-ref) 1))
             (new-ref (if (and (stringp new-ref)
                               (member new-ref existing-branches))
                          (while (< tries 3)
                            (cl-incf tries)
                            (consult--read (list initial)
                                         :prompt (format "A branch with that name already exists. Pick a different name (attempt %s/3): " tries)
                                         :sort nil))
                        new-ref))
             (_ (if (not new-ref) (user-error "Did not get a valid branch name")))
             (ref (or ref (consult-gh--read-branch new-repo nil "Select reference branch for starting point: " t nil)
                      "HEAD"))
             (sha (or (and (stringp ref) (get-text-property 0 :sha ref))
                      (and (stringp ref)
                           (ignore-errors (gethash :sha (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/branches/%s" repo ref)) :commit))))))
             (args (list "api" "-X" "POST")))
         (when (and new-repo new-ref sha)
           (setq args (append args (list (format "/repos/%s/git/refs" new-repo)
                                    "-f" (format "ref=refs/heads/%s" new-ref)
                                    "-f" (format "sha=%s" sha))))

           (consult-gh--make-process (format "consult-gh-create-branch-%s" new-repo)
                                :when-done (lambda (_event _str)
                                              (and
                                               (consult-gh--rename-commit-submit files new-repo commit-message new-ref committer-info author-info)
                                               (message "Creating a Pull Request...")
                                               (consult-gh-pr-create repo "Create/Updates" nil ref new-repo new-ref)))
                                      :cmd-args args)))))))

(defun consult-gh--rename-commit-single-file (file repo &optional commit-message ref committer-info author-info)
  "Submit a commit to rename a single file.

Description of Arguments:
  FILE               a string with properties; the properties describe details
                     abount path and content of the file.
  REPO               a string; full name of repository
  COMMIT-MESSAGE     a string; commit message
  REF                a string; name of branch ref for commit
  COMMITTER-INFO     a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO        a plist; author info plist, (:name NAME :email EMAIL)"

  (let* ((repo (or repo (get-text-property 0 :repo file)))
         (old-path (get-text-property 0 :old-path file))
         (new-path (get-text-property 0 :new-path file))
         (mode (get-text-property 0 :mode file))
         (ref (or ref (get-text-property 0 :ref file)))
         (committer-name (when (hash-table-p committer-info) (gethash :name committer-info)))
         (committer-email (when (hash-table-p committer-info) (gethash :email committer-info)))
         (author-name (when (hash-table-p author-info) (gethash :name author-info)))
         (author-email (when (hash-table-p author-info) (gethash :email author-info)))
         (old-url (format "/repos/%s/contents/%s" repo old-path))
         (old-url-ref (if ref (concat old-url (format "?ref=%s" ref)) old-url))
         (old-api-response (consult-gh--api-get-json old-url-ref)))

    (when (eq (car old-api-response) 0)
        (let* ((resp (consult-gh--json-to-hashtable (cadr old-api-response) (list :path :sha)))
               (shas (cond
                      ((hash-table-p resp)
                       (list resp))
                      ((listp resp)
                       resp))))
          (when (and repo shas)
            (cl-loop for item in shas
                     collect
                     (let* ((item-old-path (gethash :path item))
                            (item-sha (gethash :sha item))
                            (item-new-path (cond
                                            ((equal mode "directory")
                                             (and (stringp item-old-path)
                                                  (stringp new-path)
                                                  (stringp (file-name-nondirectory item-old-path))
                                                  (concat (or (file-name-as-directory new-path) "") (string-remove-prefix "/" (string-remove-prefix old-path item-old-path)))))
                                            (t
                                             new-path)))
                            (commit-message (or (and (stringp commit-message)
                                                     (not (string-empty-p (string-trim commit-message)))
                                                     (concat commit-message
                                                             (format "\n rename %s to %s\n"
                                                                     item-old-path item-new-path)))
                                                (and (stringp item-old-path)
                                                     (format "Rename %s to %s\n" item-old-path item-new-path))
                                                (consult--read nil
                                                               :prompt "Commit Message: "
                                                               :sort nil)))
                            (new-url (format "/repos/%s/contents/%s" repo item-new-path))
                            (new-url-ref (if ref (concat new-url (format "?ref=%s" ref)) new-url))
                            (new-api-response (consult-gh--api-get-json new-url-ref))
                            (new-sha  (if (eq (car new-api-response) 0)
                                          (consult-gh--json-to-hashtable (cadr new-api-response) :sha)))
                            (viewbuffer (consult-gh--files-view repo item-old-path nil t nil nil ref))
                            (content  (and (bufferp viewbuffer)
                                           (buffer-live-p viewbuffer)
                                       (with-current-buffer viewbuffer
                                         (save-restriction
                                           (widen)
                                           (buffer-substring-no-properties (point-min) (point-max))))))
                            (base64-content (and (stringp content)
                                              (base64-encode-string (substring-no-properties (encode-coding-string content 'utf-8)))))

                            (delete-args (list "api"
                                        "-H" "Accept: application/vnd.github+json"
                                        "--method" "DELETE"
                                        (format "/repos/%s/contents/%s" repo item-old-path)
                                        "-f" (format "message=%s" commit-message)
                                        "-f" (format "sha=%s" item-sha)))
                            (create-args (list "api"
                                               "-H" "Accept: application/vnd.github+json"
                                               "--method" "PUT"
                                               new-url
                                               "-f" (format "message=%s" commit-message)
                                               "-f" (format "content=%s" base64-content))))
                       (when ref (setq delete-args (append delete-args (list "-f" (format "branch=%s" ref)))
                                       create-args (append create-args (list "-f" (format "branch=%s" ref)))))
                       (when (and committer-name committer-email)
                         (setq delete-args (append delete-args (list "-f" (format "committer[name]=%s" committer-name)
                                                       "-f" (format "committer[email]=%s" committer-email)))
                               create-args (append create-args (list "-f" (format "committer[name]=%s" committer-name)
                                    "-f" (format "committer[email]=%s" committer-email)))))
                       (when (and author-name author-email)
                         (setq delete-args (append delete-args (list "-f" (format "author[name]=%s" author-name)
                                                       "-f" (format "author[email]=%s" author-email)))
                               create-args (append create-args (list "-f" (format "author[name]=%s" author-name)
                                    "-f" (format "author[email]=%s" author-email)))))

                       (when new-sha (setq create-args (append create-args (list "-f" (format "sha=%s" new-sha)))))


                       (and
                        (if (eq (car new-api-response) 0)
                            (y-or-n-p (format "The file %s already exists.  Are you sure you want to override it?" item-new-path))
                          t)
                        (not (equal item-new-path item-old-path))
                        (apply #'consult-gh--command-to-string create-args)
                        (message "File %s renamed to %s!" (propertize item-old-path 'face 'consult-gh-date)  (propertize item-new-path 'face 'consult-gh-warning))

                        (apply #'consult-gh--command-to-string delete-args)))))))))

(defun consult-gh--rename-commit-submit (files repo commit-message &optional ref committer-info author-info)
  "Submit a rename FILES commit in REPO.

Description of Arguments:
  FILES          a list of propertized strings; each string contains
                 information \(e.g. path, content, ...) for 1 file.
  REPO           a string; full name of repository
  COMMIT-MESSAGE a string; commit message
  REF            a string; name of branch ref for commit
  COMMITTER-INFO a plist; committer info plist, (:name NAME :email EMAIL)
  AUTHOR-INFO    a plist; author info plist, (:name NAME :email EMAIL)"
  (pcase-let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
               (canwrite (consult-gh--user-canwrite repo))
               (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
               (commit-message (or (and (stringp commit-message)
                                        (not (string-empty-p (string-trim commit-message)))
                                        commit-message)
                                   (and (listp files)
                                        (format "Rename Files\n\n%s\n" (mapconcat (lambda (item) (format " - rename %s to %s" (get-text-property 0 :old-path item) (get-text-property 0 :new-path item))) files "\n")))
                                   (consult--read nil
                                                  :prompt "Commit Message: "
                                                  :sort nil)))
               (protected (when ref (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/branches/%s" repo ref)) :protected))))

    (when (and commit-message
               (stringp commit-message)
               (not (string-empty-p commit-message)))
      (consult-gh-commit-save-message commit-message))

    (cond
     ((not canwrite)
      (let* ((info (consult-gh--command-to-string "repo" "view" repo "--json" "isFork,isPrivate"))
             (isForked (equal (consult-gh--json-to-hashtable info :isFork) 't))
             (isPrivate (equal (consult-gh--json-to-hashtable info :isPrivate) 't)))
        (when (and (not isForked) (not isPrivate))
          (and (y-or-n-p (format "User %s does not have write acccess in %s.  Do you want to fork the repo and make a pull request?" user repo))
               (consult-gh--rename-commit-by-fork files repo commit-message ref committer-info author-info)))))
     ((equal protected t)
      (and (y-or-n-p "The branch you are trying to edit is protected.  Do you want to submit a pull request? ")
           (consult-gh--rename-commit-by-pullrequest files repo commit-message ref committer-info author-info)))
     (t
      (when (and canwrite repo (listp files) commit-message)
        (let* ((confirm))
          (cl-loop for file in files
                   collect
                   (let* ((old-path (get-text-property 0 :old-path file))
                          (new-path (get-text-property 0 :new-path file))
                          (ref (or ref (get-text-property 0 :ref file))))
                     (unless (equal confirm "all")
                       (setq confirm (read-answer (concat "This will rename "
                                                          (format "%s to %s on GitHub.  Are you sure you want to continue? " (propertize old-path 'face 'consult-gh-warning)
                             (propertize new-path 'face 'consult-gh-date)))
                               '(("yes"  ?y "rename the file")
                                 ("no"   ?n "skip to the next file")
                                 ("all"  ?! "accept all remaining without more questions (will rename existing files!)")
                                 ("help" ?h "show help")
                                 ("quit" ?q "exit")))))
                     (cond
                      ((or (equal confirm "yes")
                           (equal confirm "all"))

                       (consult-gh--rename-commit-single-file file repo commit-message ref committer-info author-info))
                      ((equal confirm "no")
                       (message "Skipped %s" (propertize file 'face 'consult-gh-error)))
                      (t (message "Canceled!")))))))))))

(defun consult-gh--rename-commit-add-file (&optional commit file)
  "Add FILE to the COMMIT for renaming files."
  (if consult-gh-topics-edit-mode
      (let* ((commit (or commit consult-gh--topic))
             (repo (get-text-property 0 :repo commit))
             (ref (or (get-text-property 0 :commit-ref commit)
                      (get-text-property 0 :ref commit)))
             (files (get-text-property 0 :files commit))
             (old-file (or file
                           (consult-gh--files-read-file repo ref (and (listp files)
                                                                      (stringp (get-text-property 0 :old-path (car files)))
                                                                      (file-name-directory (get-text-property 0 :old-path (car files))))
                                                        nil "Select a File: " t nil nil 'branch)))
             (old-path (and (stringp old-file)
                            (not (string-empty-p old-file))
                            (get-text-property 0 :path old-file)))
             (old-path (and (stringp old-path)
                            (not (string-empty-p old-path))
                            old-path))
             (new-file (consult-gh--files-read-file repo ref (and (stringp old-path)
                                                                  (file-name-directory old-path))
                                                    (and (stringp old-path)
                                                         (file-name-nondirectory old-path))
                                                    "Enter new path: " nil nil nil 'branch))
             (new-path (and (stringp new-file)
                            (not (string-empty-p new-file))
                            (get-text-property 0 :path new-file)))
             (new-path (and (stringp new-path)
                            (not (string-empty-p new-path))
                            new-path))
             (view-buffer (consult-gh--files-view repo old-path nil t nil nil ref))
             (content (with-current-buffer view-buffer
                        (widen)
                        (buffer-substring-no-properties (point-min) (point-max))))
             (_ (when (stringp new-path)
                  (add-text-properties 0 1 (list :repo repo
                                                 :old-path old-path
                                                 :new-path new-path
                                                 :api-url nil
                                                 :mode "file"
                                                 :size nil
                                                 :ref ref
                                                 :sha nil
                                                 :class "file"
                                                 :type "file"
                                                 :object-type "blob"
                                                 :content content
                                                 :view-buffer view-buffer
                                                 :new t)
                                       new-path)))
             (new-files (remove nil (cl-remove-duplicates (append (list (and (stringp new-path) new-path))
                                                                  files)
                                                          :test #'equal))))
        (add-text-properties 0 1 (list :files new-files) commit)
        (save-excursion
          (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                     (goto-char (car-safe (car-safe regions)))))
          (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
          (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
          (when (listp new-files)
            (insert (propertize "\n\n" :consult-gh-commit-instructions t))
            (insert (propertize "# Files to Rename:" :consult-gh-commit-instructions t))
            (mapc (lambda (f)
                    (add-text-properties 0 1 (list :commit-buffer (current-buffer)) f)
                    (consult-gh--commit-rename-make-diff-buffer repo f ref)
                    (insert (propertize (format "\n# - rename %s -> %s" (propertize old-path 'face 'warning) (propertize new-path 'face 'success)) :consult-gh-commit-instructions t)))
            new-files))))))

(defun consult-gh--rename-commit-remove-file (&optional commit)
  "Remove file from the files to rename in COMMIT."
  (if consult-gh-topics-edit-mode
      (let* ((commit (or commit consult-gh--topic))
             (files (get-text-property 0 :files commit))
             (new-files (remove (consult--read files
                                               :prompt "Remove Files: "
                                               :lookup #'consult--lookup-member)
                                files)))
        (add-text-properties 0 1 (list :files new-files)
                             commit)
        (save-excursion
          (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                     (goto-char (car-safe (car-safe regions)))))
          (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
          (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
          (when (listp new-files)
            (insert (propertize "\n\n" :consult-gh-commit-instructions t))
            (insert (propertize "# Files to Rename:" :consult-gh-commit-instructions t))
            (mapc (lambda (f)
                      (insert (propertize (format "\n# - rename %s -> %s" (propertize (get-text-property 0 :old-path f) 'face 'warning) (propertize (get-text-property 0 :new-path f) 'face 'success)) :consult-gh-commit-instructions t)))
                  new-files))))))

(defun consult-gh--rename-commit-presubmit (&optional commit)
  "Prepare COMMIT to submit.

COMMIT is a string with properties that identify a commit.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--rename-create'."
  (if consult-gh-commit-message-mode
      (let* ((commit (or commit
                       (and (stringp consult-gh--topic)
                            (equal (get-text-property 0 :type consult-gh--topic) "commit message")
                            consult-gh--topic)))
             (repo (get-text-property 0 :repo commit))
             (ref (or (get-text-property 0 :commit-ref commit)
                     (get-text-property 0 :ref commit)))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (user-info (consult-gh--get-user-info user))
             (committer-info (or (get-text-property 0 :committer-info commit) user-info))
             (committer (or (and (hash-table-p committer-info)
                                 (or (gethash :name committer-info)
                                     (gethash :email committer-info))
                                 (concat (gethash :name committer-info) " - " (gethash :email committer-info)))
                            user))
             (author-info  (or (get-text-property 0 :author-info commit)
                               user-info))
             (author (or (and (hash-table-p author-info)
                              (or (gethash :name author-info)
                                (gethash :email author-info))
                          (concat (gethash :name author-info) " - " (gethash :email author-info)))
                           user))
             (nextsteps (append (list (cons "Submit" :submit))
                                (list (cons "Add Files" :add))
                                (list (cons "Remove Files" :remove))
                                (list (cons (format "Change Branch (current: %s)" ref) :ref))
                                (list (cons (format "Change Committer (current: %s)" committer) :committer))
                                (list (cons (format "Change Author (current: %s)" author) :author))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil))

             (buffer (current-buffer)))

          (pcase next
            (':cancel)
            (':submit
             (let* ((files (get-text-property 0 :files consult-gh--topic))
                    (ref (or (get-text-property 0 :commit-ref consult-gh--topic)
                                (get-text-property 0 :ref consult-gh--topic)))
                    (commit-message (consult-gh--commit-get-buffer-message)))
               (and (consult-gh--rename-commit-submit files repo commit-message ref committer-info author-info)
                    (message "Commit %s" (propertize "Submitted!" 'face 'consult-gh-success))
                    (with-current-buffer buffer
                    (funcall consult-gh-quit-window-func t)))))
            (':add (consult-gh--rename-commit-add-file commit))
            (':remove (consult-gh--rename-commit-remove-file commit))
            (':committer (consult-gh--commit-change-committer)
                         (consult-gh--create-commit-presubmit))
            (':author (consult-gh--commit-change-author)
                      (consult-gh--create-commit-presubmit))
            (':ref (consult-gh--commit-change-ref)
                      (consult-gh--create-commit-presubmit))))
    (message "Not a Github commit buffer!")))

(defun consult-gh--search-commits-format (string input highlight)
  "Format candidates for commits.

Description of Arguments:

  STRING the output of a “gh” call
         \(e.g. “gh search commits ...”\).
  INPUT  the query from the user
         \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
           with `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "commit")
         (type "commit")
         (parts (string-split string "\t"))
         (repo (car parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (sha (cadr parts))
         (sha-str (and (stringp sha)
                       (substring sha 0 6)))
         (message (cadr (cdr parts)))
         (author (cadr (cddr parts)))
         (date (cadr (cdddr parts)))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width message 70)
                      (propertize sha-str 'face 'consult-gh-sha)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize (consult-gh--set-string-width author 15) 'face 'consult-gh-pr)
                      (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user )) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo
                     :user user
                     :package package
                     :sha sha
                     :author author
                     :commit-message message
                     :date date
                     :query query
                     :class class
                     :type type)
                         str)
    str))

(defun consult-gh--commit-group (cand transform)
  "Group function for commit.

This is passed as GROUP to `consult--read' in
`consult-gh-search-commits' and is used to group commits.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-commit-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Message " 68 nil ?-)
       (consult-gh--set-string-width " Sha " 8 nil ?-)
       (consult-gh--set-string-width " Date " 12 nil ?-)
       (consult-gh--set-string-width " Author "17 nil ?-)
       (consult-gh--set-string-width " Repo " 40 nil ?-))))))

(defun consult-gh--commit-state ()
  "State function for commit candidates.

This is passed as STATE to `consult--read' in `consult-gh-search-commits'
and is used to preview or do other actions on the issue."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (sha (get-text-property 0 :sha cand))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (consult-gh--commit-view (format "%s" repo) (format "%s" sha) buffer t)
               (with-current-buffer buffer
                 (outline-hide-sublevels 2))
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun  consult-gh--commit-browse-url-action (cand)
  "Browse the url for a commit candidate, CAND.

This is an internal action function that gets a commit candidate, CAND,
from `consult-gh-search-commits' and opens the url of the commit
in an external browser.

To use this as the default action for commits,
set `consult-gh-commit-action' to `consult-gh--commit-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (sha (substring-no-properties (get-text-property 0 :sha cand)))
         (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")))
         (url (and repo-url (concat repo-url "/commit/" sha))))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--commit-read-hashtable (repo sha &optional keys)
"Geet details of commit SHA in REPO.

Whe KEYS is a list of keywords, only retrieve fields in KEYS."
(apply #'consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/commits/%s" repo sha)) keys))

(defun consult-gh--commit-get-comments (repo sha)
  "Get comments of commit SHA in REPO.

Retrieves a list of comments issue with id NUMBER in REPO.
Optional argument maxnum limits the number of comments retrieved."
   (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/commits/%s/comments" repo sha))))

(defun consult-gh--commit-get-commenters (table &optional comments)
  "Get a list of related users to a commit.

Retrieves a list of all related commenter users for the comment
stored in TABLE, a hash-table output
from `consult-gh--commit-read-hashtable'.
Optional argument COMMENTS is a list of comments
from `consult-gh--commit-get-comments'.”"
  (let* ((author (gethash :login (gethash :author table)))
         (commenters (when (and comments (listp comments))
                       (cl-loop for comment in comments
                                collect
                                (when (hash-table-p comment)
                                  (gethash :login (gethash :user comment)))))))
         (cl-remove-duplicates (delq nil (append (list author) commenters)) :test #'equal)))

(defun consult-gh--commit-get-parents (table)
  "Get parents of the  commit in TABLE.

TABLE is a hash-table output containing issue information
from `consult-gh--commit-read-hashtable'.  Returns a formatted string containing
the header section for `consult-gh--commit-view'."

 (let* ((parents (gethash :parents table))
        (parents-list (when (and parents
                                 (listp parents))
                        (cl-loop for parent in parents
                                 collect
                                 (when (hash-table-p parent)
                                   (let* ((parent-sha (gethash :sha parent))
                                          (html-url (gethash :html_url parent))
                                          (parent-sha-str (and (stringp parent-sha)
                                                               (substring parent-sha 0 6))))
                                          (when (stringp parent-sha-str)
                                            (propertize parent-sha-str :sha parent-sha :url html-url))))))))
   (cl-remove-duplicates (delq nil parents-list) :test #'equal)))

(defun consult-gh--commit-read-filter-comments-query (comments &optional maxnum)
  "Filter COMMENTS when there are more than MAXNUM.

Queries the user for how to filter the comments."
  (let* ((maxnum (or maxnum consult-gh-comments-maxnum)))
    (when (and (listp comments) (> (length comments) maxnum))
      (pcase (consult--read (list (cons "Yes, Load Everything" :nil)
                                  (cons (format "No, Load up to %s latest comments." maxnum) :last-maxnum)
                                  (cons "No, let me enter the number of commetns to load" :last-number)
                                  (cons "No, only load the comments in the last week." :last-week)
                                  (cons "No, only load the comments in the last month." :last-month)

                                  (cons "No, only load the comments since a date I choose" :date)
                                  (cons "No, only load the comments in a date range I choose" :daterange))
                            :prompt (format "There are more than %s comments on that pull request. Do you want to load them all?" maxnum)
                            :lookup #'consult--lookup-cdr
                            :sort nil)
        (':last-week
         (setq comments (cl-remove-if-not (lambda (k)
                                            (time-less-p (encode-time (decoded-time-add (decode-time (current-time) t) (make-decoded-time :day -7))) (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                          comments)))
        (':last-month
         (setq comments (cl-remove-if-not (lambda (k)
                                            (time-less-p (encode-time (decoded-time-add (decode-time (current-time) t) (make-decoded-time :day -30))) (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                          comments)))
        (':last-maxnum
         (setq comments (cl-subseq comments (max 0 (- (length comments) maxnum)))))
        (':last-number
         (let ((number (read-number "Enter the number of comments to load: ")))
           (when (numberp number)
             (setq comments (cl-subseq comments 0 (min (length comments) (truncate number)))))))
        (':date
         (let* ((limit-begin (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car comments)) (gethash :created_at (car comments))))))
               (limit-end (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car (last comments))) (gethash :created_at (car (last comments)))))))
               (d (org-read-date nil t nil (format "Select Begin Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date)))))
           (setq comments (cl-remove-if-not (lambda (k)
                                              (time-less-p d (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                            comments))))
        (':daterange
         (let* ((limit-begin (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car comments)) (gethash :created_at (car comments))))))
               (limit-end (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car (last comments))) (gethash :created_at (car (last comments)))))))
               (begin-date (org-read-date nil t nil (format "Select Begin Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date))))
               (end-date (org-read-date nil t nil (format "Select End Date range - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date)))))
           (setq comments (cl-remove-if-not (lambda (k)
                                              (let ((date (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                                (and (time-less-p begin-date date) (time-less-p date end-date))))
                                            comments)))))
    comments)))

(defun consult-gh--commit-filter-comments (comments &optional maxnum)
  "Filter COMMENTS when there are more than MAXNUM.

Queries the user for how to filter the comments."
  (when (and comments (listp comments))
  (pcase consult-gh-commits-show-comments-in-view
    ('all comments)
    ((pred (lambda (var) (numberp var)))
     (cl-subseq comments (max 0 (- (length comments) consult-gh-commits-show-comments-in-view)
                          (- (length comments) (or maxnum consult-gh-comments-maxnum)))))
    (_ (consult-gh--commit-read-filter-comments-query comments maxnum)))))

(defun consult-gh--commit-format-comments (comments &optional url)
  "Format the COMMENTS for a commit.

The optional argument URL, is the web url for the commit on GitHub."
  (let* ((header-marker "#")
         (out nil))
    (when (listp comments)
      (cl-loop for comment in comments
               do
               (when (hash-table-p comment)
                 (let* ((author (gethash :user comment))
                        (author (and author (gethash :login author)))
                        (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author))))
                        (comment-url (gethash :html_url comment))
                        (id (gethash :id comment))
                        (authorAssociation (gethash :author_association comment))
                        (authorAssociation (unless (equal authorAssociation "NONE")
                                             authorAssociation))
                        (createdAt (or (gethash :updated_at comment)
                                       (gethash :created_at comment)
                                       (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))
                        (createdAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time createdAt)))

                        (oid (gethash :commit_id comment))
                        (path (gethash :path comment))
                        (position (gethash :position comment))
                        (line (gethash :line comment))
                        (body (gethash :body comment)))

                   (save-match-data
                     (when (and body (string-match (concat "^" header-marker "+?\s.*$")  body))
                       (setq body (with-temp-buffer
                                    (insert body)
                                    (goto-char (point-min))
                                    (while (re-search-forward (concat "^" header-marker "+?\s.*$") nil t)
                                      (replace-match (concat header-marker header-marker "\\&")))
                                    (buffer-string)))))
                   (setq out (concat out (propertize (concat header-marker header-marker " "
                                                             (and author (concat author " "))
                                                             (and authorAssociation (concat "(" authorAssociation ")"))
                                                             (and createdAt (concat (consult-gh--time-ago createdAt) " " createdAt))
                                                             "\n"

(and oid (concat "\ncommit: " "[" (substring oid 0 6) "]" (format "(%s/commits/%s)" url oid)
                 (and path (format " path: %s  " path))
                 (and position (format  " position: %s " position))
                 (and line (format " line: %s " line))
                 "\n-----"))
(and body (concat "\n" body "\n"))
"\n----------\n")
                                                     :consult-gh (list :author author :comment-url comment-url :comment-id id :position position :line line :path path :commit-url (when oid (format "%s/commits/%s" url oid))))))))))
    out))

(defun consult-gh--commit-comments-section (comments-text comments comments-filtered &optional preview)
  "Format the comments section with COMMENTS-TEXT.

Add a placeholder for loading the rest, when PREVIEW is non-nil or if
length of COMMENTS is larger than length of COMMENTS-FILTERED."
  (if (or preview (not consult-gh-commits-show-comments-in-view))
      (pcase consult-gh-commit-preview-major-mode
        ((or 'gfm-mode 'markdown-mode)
         (concat "\n"
                 (propertize "## " :consult-gh-pr-comments t)
                 (buttonize (propertize "Use **M-x consult-gh-commit-view-comments** to Load Comments..." :consult-gh-commit-comments t) (lambda (&rest _) (consult-gh-commit-view-comments)))
                 "\n"))
        ('org-mode
         (concat "\n"
                 (propertize "## " :consult-gh-commit-comments t)
                 (buttonize (propertize "Load Comments..." :consult-gh-commit-comments t) (lambda (&rest _) (consult-gh-commit-view-comments)))
                 "\n")))
    (cond
     ((and (listp comments) (listp comments-filtered) (> (length comments) (length comments-filtered)))
      (pcase consult-gh-commit-preview-major-mode
        ((or 'gfm-mode 'markdown-mode)
         (concat "\n"
                 (when (stringp comments-text) (propertize comments-text :consult-gh-commit-comments t))
                 "\n"
                 (propertize "## " :consult-gh-commit-comments t)
                 (buttonize (propertize "Use **M-x consult-gh-commit-view-comments** to load the more..." :consult-gh-commit-comments t) (lambda (&rest _) (consult-gh-commit-view-comments)))
                 "\n"))
        ('org-mode
         (concat "\n"
                 (when (stringp comments-text) (propertize comments-text :consult-gh-commit-comments t))
                 (propertize "\n" :consult-gh-commit-comments t)
                 (propertize "## " :consult-gh-commit-comments t)
                 (buttonize (propertize "Load More..." :consult-gh-commit-comments t) (lambda (&rest _) (consult-gh-commit-view-comments)))
                 "\n"))))
     (t
      (concat "\n"
              (if (stringp comments-text)
                  (propertize comments-text :consult-gh-commit-comments t)
                (propertize "No Comments!" :consult-gh-commit-comments t))
              "\n")))))

(defun consult-gh--commit-format-header (repo sha table &optional topic)
  "Format a header for commit with SHA in REPO.

TABLE is a hash-table output containing issue information
from `consult-gh--commit-read-hashtable'.  Returns a formatted string containing
the header section for `consult-gh--commit-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--commit-view'."
  (let* ((author (ignore-errors (gethash :login (gethash :author table))))
         (commit (gethash :commit table))
         (date (and (hash-table-p commit)
                    (ignore-errors (gethash :date (gethash :committer commit)))))
         (date (and date (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time date))))
         (tree (and (hash-table-p commit)
               (gethash :tree commit)))
         (message (gethash :message commit))
         (message (and (stringp message) (car (split-string message "\n"))))
         (url (gethash :html_url table)))

    (when (stringp topic)
      (add-text-properties 0 1 (list :author author :date date :tree tree) topic))

    (concat "commit: " (and (stringp sha) (substring sha 0 6)) "\n"
            "message: " (and (stringp message) message) "\n"
            "author: " author "\n"
            "repository: " (propertize repo 'help-echo (apply-partially #'consult-gh--get-repo-tooltip repo)) "\n"
            "sha: " sha "\n"
            (and date (concat "date: " date "\n"))
            (and url (concat "url: " url "\n"))
            "\n--\n")))

(defun consult-gh--commit-format-body (table &optional topic)
  "Format a body section for a commit stored in TABLE.

This function returns a formatted string containing the body section for
`consult-gh--commit-view'.

TABLE is a hash-table output from `consult-gh--commit-read-hashtable'
containing commit details under the key :commit.

The optional argument TOPIC is a propertized text where the related info
from the commit will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--commit-view'."
  (let* ((author (ignore-errors (gethash :login (gethash :author table))))
         (commit (gethash :commit table))
         (date (and (hash-table-p commit)
                    (ignore-errors (gethash :date (gethash :committer commit)))))
         (date (and date (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time date))))
         (body (and (hash-table-p commit) (gethash :message commit)))
         (header-marker "#"))

    (when topic (add-text-properties 0 1 (list :body body) topic))

     (save-match-data
                     (when (and body (string-match (concat "^" header-marker "+?\s.*$")  body))
                       (setq body (with-temp-buffer
                                    (insert body)
                                    (goto-char (point-min))
                                    (while (re-search-forward (concat "^" header-marker "+?\s.*$") nil t)
                                      (replace-match (concat header-marker "\\&")))
                                    (buffer-string)))))

    (concat author " " (consult-gh--time-ago date)
            " " (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time date)) "\n"
            "-----\n" body "\n" "\n")))

(defun consult-gh--commit-format-parents (parents sha &optional topic)
  "Format parents section for commit view.

PARENTS is a list of parent commits of commit
SHA is SHA-1 of commit.

This function returns a formatted string containing the parents section for
`consult-gh--commit-view'.

The optional argument TOPIC is a propertized text where the related info
from the commit will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--commit-view'."
  (let* ((count (and (listp parents) (length parents))))
    (when topic (add-text-properties 0 1 (list :parents parents) topic))
    (concat (and (numberp count) (number-to-string count))
            " parents "
            (mapconcat (lambda (p)
                         (when (stringp p)
                           (concat "[" (substring p 0 6) "]"
                                   (format "(%s)" (get-text-property 0 :url p)))))
                       parents " + ")
            (and (stringp sha) (concat " commit " (substring sha 0 6)))
            "\n\n")))

(defun consult-gh--commit-format-files-changed (table)
  "Format a changed files section for a commit.

TABLE is a hash-table output containing commit information
from `consult-gh--commit-read-hashtable'.  Returns a formatted string
containing the files changed section for `consult-gh--commit-view'."
  (let* ((stats (gethash :stats table))
         (files (gethash :files table))
        (total (and (listp files) (length files)))
        (additions (and (hash-table-p stats) (gethash :additions stats)))
        (deletions  (and (hash-table-p stats) (gethash :deletions stats))))
    (and stats (concat "# Files Changed - "
                       (and total (format "%s file(s)" total))
                       (and additions (format ", %s additions(+)" additions))
                       (and deletions (format ", %s deletions(-)" deletions)) "\n"))))

(defun consult-gh--commit-format-diffs (repo sha table &optional header-level preview)
  "Format diffs or commit SHA in REPO.

This is used for preparing the text section of diffs in
`consult-gh--commit-view'.

Description of Arguments:
  REPO          a string; full name of repository
  SHA           a string; number id of the pull request
  TABLE         a hash-table; output containing commit
                information from `consult-gh--commit-read-hashtable'.
  HEADER-LEVEL  a number; outline level for adding the diff section
  PREVIEW       a boolean; whether this is for preview"
  (let*  ((files (gethash :files table)))
    (when (listp files)
      (with-temp-buffer
        (cl-loop for file in files
                 do
                 (when (hash-table-p file)
                   (let* ((filename (gethash :filename file))
                          (url (gethash :blob_url file))
                          (diff (gethash :patch file)))

                     (when (stringp filename)
                       (insert (propertize (concat (make-string (+ header-level 1) ?#) " " filename) :consult-gh (list :repo repo :sha sha :path filename :commit-id sha :commit-url url :file t :code nil)))
                       (if preview
                           (insert "\n")
                         (if (stringp diff)
                           (let ((start (point)))
                             (insert (concat (propertize  "\n```diff\n" :consult-gh (list :repo repo :sha sha :path filename :commit-id sha :commit-url url :file t :code nil)) (propertize diff :consult-gh (list :repo repo :sha sha :path filename :commit-id sha :commit-url url :file t :code t)) (propertize  "\n```\n" :consult-gh (list :repo repo :sah sha :path filename :commit-id sha :commit-url url :file t :code nil))))
                             (save-excursion
                               (while (re-search-backward "^\s?\\*+\s\\|^\s?#\\+" start t)
                                 (replace-match (apply #'propertize (concat  "," (match-string 0)) (text-properties-at 0 (match-string 0))) nil t))))
                           (insert "\n")))))))
                 (consult-gh--whole-buffer-string)))))

(defun consult-gh--commit-view (repo sha &optional buffer preview)
  "Open commit with SHA of REPO in an Emacs buffer, BUFFER.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and SHA,
a SHA-1 hash of a commit in that repository, and shows
the contents of the commit in an Emacs buffer.

Description of Arguments:

  REPO    a string; the full name of the repository
  SHA     a string; SHA-1 hash of commit
  BUFFER  a string; optional buffer name
  PREVIEW a boolean; whether to load reduced preview
  TITLE   a string; an optional title string

To use this as the default action for commits,
see `consult-gh--commit-view-action'."
  (let* ((topic (format "%s/commit/%s" repo sha))
         (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
         (table (consult-gh--commit-read-hashtable repo sha))
         (comments (when (and consult-gh-commits-show-comments-in-view (not preview))
                     (consult-gh--commit-get-comments repo sha)))
         (comments-filtered (when comments (consult-gh--commit-filter-comments comments)))
         (commenters (and table (not preview) (consult-gh--commit-get-commenters table comments)))
         (parents (and table (consult-gh--commit-get-parents table)))
         (header-text (and table (consult-gh--commit-format-header repo sha table topic)))
         (body-text (consult-gh--commit-format-body table topic))
         (parents-text (and parents (consult-gh--commit-format-parents parents sha topic)))
         (file-change-text (consult-gh--commit-format-files-changed table))
         (diff-text (consult-gh--commit-format-diffs repo sha table 1 preview))
         (comments-text (when (and comments-filtered (listp comments-filtered))
                           (consult-gh--commit-format-comments comments-filtered (gethash :html_url table))))
         (comments-section (consult-gh--commit-comments-section comments-text comments comments-filtered preview)))
    (add-text-properties 0 1 (list :repo repo :type "commit" :commenters (mapcar (lambda (item) (concat "@" item)) commenters) :sha sha :view "commit") topic)

    (unless preview
      (consult-gh--completion-set-all-fields repo topic (consult-gh--user-canwrite repo)))

    (with-current-buffer buffer
      (let ((inhibit-read-only t))
        (erase-buffer)
        (fundamental-mode)
        (when header-text (insert header-text))
        (save-excursion
          (when (eq consult-gh-commit-preview-major-mode 'org-mode)
           (consult-gh--github-header-to-org buffer)))
        (when body-text (insert body-text))
        (when parents-text (insert parents-text))
        (when file-change-text (insert file-change-text))
        (when diff-text (insert diff-text))
        (when comments-section (insert "# Comments\n")
              (insert comments-section))
        (consult-gh--format-view-buffer "commit")
        (outline-hide-sublevels 1)
        (consult-gh-commit-view-mode +1)
        (setq-local consult-gh--topic topic)
        (current-buffer)))))

(defun consult-gh--commits-view-action (cand)
  "Open the preview of a commit candidate, CAND.

This is a wrapper function around `consult-gh--commit-view'.
It parses CAND to extract relevant values
\(e.g. repository's name and commit sha\)
and passes them to `consult-gh--commit-view'.

To use this as the default action for commits,
set `consult-gh-commit-action' to `consult-gh--commit-view-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (sha (substring-no-properties (get-text-property 0 :sha cand)))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/commits/" sha "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the commit in the existing buffer." :replace)
                             (cons "Make a new buffer and load the commit in it (without killing the old buffer)." :new))
                       :prompt "You already have this commit open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))

(if existing
      (cond
       ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
       ((eq confirm :replace)
        (message "Reloading commit in the existing buffer...")
        (funcall consult-gh-switch-to-buffer-func (consult-gh--commit-view repo sha existing))
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer)))
       ((eq confirm :new)
        (message "Opening issue in a new buffer...")
        (funcall consult-gh-switch-to-buffer-func (consult-gh--commit-view repo sha (generate-new-buffer buffername nil)))
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--commit-view repo sha))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--commit-find-file (repo &optional sha path)
  "Browse files in COMMIT.

Description of Arguments:

  REPO          a string; repository's full name
                \(e.g., armindarvish/consult-gh\)
  SHA           a string; commit sha
  PATH          a string; when non-nil search is done relative to PATH"
  (let* ((commit (unless sha
                   (consult-gh-commit-list repo nil nil t)))
         (sha (or sha
                  (and (stringp commit)
                       (get-text-property 0 :sha commit)))))

  (consult-gh--files-read-file repo sha path)))

(defun consult-gh--commit-browse-files-action (cand)
  "Browse file tree of a commmit candidate, CAND.

Query the user to select a file from the file tree of repo at commit.

This is a wrapper function around `consult-gh-commit-browse-files'.
To use this as the default action for commits,
set `consult-gh-commit-action' to `consult-gh-commit-browse-files'."
    (consult-gh-commit-browse-files cand))

(defun consult-gh--commit-list-commits-and-links (repo ref api-link)
  "Get list of commits for REPO and REF.

REF is name of a branch or tag in REPO.
If optional argument API-LINK is non-nil, use link instead of
REPO and REF."
  (let* ((commits (consult-gh--command-to-string "api" "-H" "Accept: application/vnd.github+json" "--include" (or api-link (format "repos/%s/commits?sha=%s&per_page=%s" repo ref (or consult-gh-commit-maxnum 30)))))
         (commits-link
          (when (stringp commits)
            (with-temp-buffer
              (insert commits)
              (goto-char (point-min))
              (save-match-data
                (save-excursion
                  (re-search-forward "\\(?:\\(?:\n\\|
\n
\\)\n\\)" nil t)
                  (let* ((header (buffer-substring-no-properties (point-min) (point)))
                         (body (buffer-substring-no-properties (point) (point-max)))
                         (link-next (if (string-match ".*Link: .*<\\(?1:http.*\\)>.*?; rel=\"next\".*$" header)
 (match-string 1 header)))
                         (link-prev (if (string-match ".*Link: .*<\\(?1:http.*\\)>.*?; rel=\"prev\".*$" header)
                                        (match-string 1 header)))
                         (link-last (if (string-match ".*Link: .*<\\(?1:http.*\\)>.*?; rel=\"last\".*$" header)
                                        (match-string 1 header)))
                         (link-first (if (string-match ".*Link: .*<\\(?1:http.*\\)>.*?; rel=\"first\".*$" header)
                                        (match-string 1 header)))
                         (body (and (stringp body)
                                    (consult-gh--json-to-hashtable body))))
                    (list body link-next link-prev link-first link-last))))))))
    commits-link))

(defun consult-gh--commit-parse-commits-and-links (commits-and-links)
  "Parse list of COMMITS-AND-LINKS.

First elemnt of COMMITS-AND-LINKS is a list of hah-tables
with details of each commit.
Second elemnt of COMMITS-AND-LINKS is a link to the previous
page on API.
Third elemnt of COMMITS-AND-LINKS is a link to the next page
on API.
Fourth elemnt of COMMITS-AND-LINKS is a link to the first page
on API.
Fifth elemnt of COMMITS-AND-LINKS is a link to the last page
on API."
  (when (listp commits-and-links)
    (let*  ((link-next (cadr commits-and-links))
            (link-prev (caddr commits-and-links))
            (link-first (cadr (cddr commits-and-links)))
            (link-last (cadr (cdddr commits-and-links)))
            (page-next (and (stringp link-next)
                          (save-match-data (when (string-match ".*page=\\(?1:.*\\)" link-next)
                                             (match-string 1 link-next)))))
            (page-next (or (and (stringp page-next)
                              (format "%s" (string-to-number page-next)))
                         page-next))
            (page-prev (and (stringp link-prev)
                          (save-match-data (when (string-match ".*page=\\(?1:.*\\)" link-prev)
                                             (match-string 1 link-prev)))))
            (page-prev (or (and (stringp page-prev)
                              (format "%s" (string-to-number page-prev)))
                         page-prev))
            (page-first (and (stringp link-first)
                          (save-match-data (when (string-match ".*page=\\(?1:.*\\)" link-first)
                                             (match-string 1 link-first)))))
            (page-first (or (and (stringp page-first)
                              (format "%s" (string-to-number page-first)))
                         page-first))
            (page-last (and (stringp link-last)
                          (save-match-data (when (string-match ".*page=\\(?1:.*\\)" link-last)
                                             (match-string 1 link-last)))))
            (page-last (or (and (stringp page-last)
                              (format "%s" (string-to-number page-last)))
                         page-last))
          (commit-prev (and link-prev
                            (make-hash-table :test 'equal)))
          (_ (when commit-prev
               (puthash :title "Previous Page" commit-prev)
               (puthash :link link-prev commit-prev)
               (puthash :page page-prev commit-prev)
               (puthash :first-page page-first commit-prev)
               (puthash :last-page page-last commit-prev)))
          (commit-next (and link-next (make-hash-table :test 'equal)))
          (_ (when commit-next
               (puthash :title "Next Page" commit-next)
               (puthash :link link-next commit-next)
               (puthash :page page-next commit-next)
               (puthash :first-page page-first commit-next)
               (puthash :last-page page-last commit-next)))
          (commits (and (listp commits-and-links)
                        (car commits-and-links)))
          (commits-list (and (listp commits)
                       (append commits
                               (list commit-next)))))
commits-list)))

(defun consult-gh--commit-list-sort (commits)
  "Sort COMMITS based on date."
(if (< emacs-major-version 30)
    (setq commits (sort commits (lambda (x y)
                                  (let* ((x_date (date-to-time (gethash :date (gethash :author (gethash :commit x)))))
                                         (y_date (date-to-time (gethash :date (gethash :author (gethash :commit y))))))
                                    (if (time-less-p x_date y_date) nil t)))))
  (setq commits (sort commits :key (lambda (k) (date-to-time (gethash :date (gethash :author (gethash :commit k))))))))
commits)

(defun consult-gh--commit-list-format (table repo ref)
  "Format candidates for listing commits.
TABLE is a hashtable containing commits info of REPO and REF.
REF is name of a branch in REPO."
  (when (hash-table-p table)
    (let* ((class "commit")
           (type "commit")
           (user (consult-gh--get-username repo))
           (package (consult-gh--get-package repo))
           (sha (gethash :sha table))
           (sha-str (and (stringp sha) (substring sha 0 6)))
           (author (gethash :author table))
           (author (and (hash-table-p author)
                        (gethash :login author)))
           (commit (gethash :commit table))
           (message (and (hash-table-p commit) (gethash :message commit)))
           (title (gethash :title table))
           (link (gethash :link table))
           (page (gethash :page table))
           (lastpage (gethash :last-page table))
           (message (or (and (stringp message)
                             (car (split-string message "\n")))
                        ""))
           (date (and (hash-table-p commit)
                      (gethash :date (gethash :author commit))))
           (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
           (str
            (if (and title (stringp title))
                (concat
                 (if title (propertize title 'face 'link))
                 (if page (concat (format " (%s" page)
                                  (if lastpage (format "/%s" lastpage))
                                  ")")))
              (concat (consult-gh--set-string-width message 70)
                          "\s\s"
                          (and sha-str (propertize sha-str 'face 'consult-gh-sha))
                          "\s\s"
                          (and date
                               (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date))
                          "\s\s"
                          (and author
                               (propertize (consult-gh--set-string-width author 15) 'face 'consult-gh-pr))
                          "\s\s"
                          (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user )) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40)))))
      (add-text-properties 0 1 (list :repo repo
                                     :user user
                                     :package package
                                     :sha sha
                                     :author author
                                     :commit-message message
                                     :date date
                                     :ref ref
                                     :title title
                                     :link link
                                     :page page
                                     :last-page lastpage
                                     :class class
                                     :type type)
                           str)
      str)))

;; Files

(defun consult-gh--files-get-trees (repo &optional path ref non-recursive cache)
  "Get a recursive git “tree” of REPO.

When PATH is non-nil, only retrieve items relative to PATH.
REF is a string for branch name, tag name or commit sha
and defaults to repsitory's “HEAD”.
When NON-RECURSIVE is non-nil, only retriev items in top folder
CACHE is string for cache duration and is passed to “gh api --cache”.
For example it can “3600s”, “60m”, or “1h”.
Uses `consult-gh--api-get-json'."
  (if (and path
           (stringp path)
           (not (string-empty-p path)))
      (let* ((ref (or ref "HEAD"))
             (parent  (or (file-name-directory path)
                          "./"))
             (parent (string-remove-suffix "/" parent))
             (info (consult-gh--api-get-command-string (format "repos/%s/git/trees/%s:?recursive=1" repo ref)))
             (tree (consult-gh--json-to-hashtable info :tree))
             (sha (cond
                   ((equal parent ".")
                    (consult-gh--json-to-hashtable info :sha))
                   ((and (listp tree)
                         (stringp parent)
                         (not (string-empty-p parent)))
                    (car-safe (remove nil (mapcar (lambda (item)
                                                    (when (hash-table-p item)
                                                      (if (equal (gethash :path item)
                                                                 parent)
                                                          (gethash :sha item))))
                                                  tree))))))
             (extra-args (if cache (list "--cache" cache))))
        (when (and repo sha)
          (apply #'consult-gh--api-get-json
           (concat "repos/" repo
                   (format "/git/trees/%s:?recursive=1" sha))
           extra-args)))
    (let ((ref (or ref "HEAD"))
          (extra-args (if cache (list "--cache" cache))))
      (apply #'consult-gh--api-get-json
       (concat "repos/" repo
               "/git/trees/" ref
               (unless non-recursive ":?recursive=1"))
       extra-args))))

(defun consult-gh--files-table-to-list (table repo &optional ref path)
  "Convert a TABLE containing git tree information of REPO PATH, and REF.

Returns a list of propertized texts
formatted properly to be sent to `consult-gh-find-file'."
  (let* ((ref (or ref "HEAD"))
         (parent  (if (stringp path)
                      (file-name-directory path)
                          "./"))
         (parent (string-remove-suffix "/" parent)))
    (mapcar (lambda (item)
              (let* ((name (gethash :path item))
                     (path (if (and (stringp path)
                                    (stringp parent))
                               (concat
                                (file-name-as-directory parent)
                                name)
                             name))
                     (api-url (gethash :url item))
                     (mode (gethash :mode item))
                     (sha (gethash :sha item))
                     (size (gethash :size item))
                     (object-type (gethash :type item))
                     (git-url (gethash :git_url item)))

              (cons name
                    (list :repo repo
                          :api-url api-url
                          :git-url git-url
                          :mode mode
                          :path path
                          :ref ref
                          :sha sha
                          :size size
                          :object-type object-type))))
            table)))

(defun consult-gh--files-list-items (repo &optional path ref non-recursive cache)
  "Fetch a list of files and directories in REPO.

When PATH is non-nil, only retrieve items relative to PATH.
REF is a string for branch name, tag name or commit sha
of the target and defaults to repo's “HEAD”
When NON-RECURSIVE is non-nil, only retrieve items in top folder.
CACHE is a string for cache duration and is passed to
`consult-gh--files-get-trees'.
Returns text with properties containing information about the file
generated by `consult-gh--files-table-to-list'.

See `consult-gh--files-nodirectory-items' for getting a list of file
but not directories."
  (let* ((ref (or ref "HEAD"))
         (response (consult-gh--files-get-trees repo path ref non-recursive cache)))
    (if (eq (car response) 0)
        (delete-dups (consult-gh--files-table-to-list
                      (consult-gh--json-to-hashtable (cadr response) :tree) repo ref path))
      (message (cadr response)))))

(defun consult-gh--files-nodirectory-items (repo &optional path ref non-recursive cache)
  "Fetch a list of non-directory files in REPO.

When PATH is non-nil, only retrieve items relative to PATH.
REF is a string for branch name, tag name or commit sha
of the target and defaults to repo's “HEAD”.
When NON-RECURSIVE is non-nil, only retriev items in top folder
CACHE is a string for cache duration and is passed to
`consult-gh--files-list-items'.
The format is propertized text that include information about the file
generated by `consult-gh--files-table-to-list'.

This list does not have directories.  See `consult-gh--files-list-items'
for getting a list of file and directories."
  (let* ((ref (or ref "HEAD"))
         (items (consult-gh--files-list-items repo path ref non-recursive cache)))
    (cl-remove-if-not (lambda (item) (equal (plist-get (cdr item) :object-type) "blob"))
                      items)))

(defun consult-gh--files-directory-items (repo &optional path ref non-recursive cache)
  "Fetch a list of directorie in REPO.

When PATH is non-nil, only retrieve items relative to PATH.
REF is a string for branch name, tag name or commit sha
of the target and defaults to repo's “HEAD”
When NON-RECURSIVE is non-nil, only retriev items in top folder
CACHE is a string for cache duration and is passed to
`consult-gh--files-list-items'.
The format is propertized text that include information about the file
generated by `consult-gh--files-table-to-list'.
This list is used for selecting a path in `consult-gh-create-file'.

This list has only directories.  See `consult-gh--files-list-items'
for getting a list of file and directories."
  (let* ((ref (or ref "HEAD"))
         (items (consult-gh--files-list-items repo path ref non-recursive cache)))
    (cl-remove-if-not (lambda (item) (equal (plist-get (cdr item) :object-type) "tree"))
                      items)))

(defun consult-gh--files-get-content-by-api-url (url)
  "Fetch the contents of file at URL retrieved from GitHub api.

Uses `consult-gh--api-get-json' and decodes it into raw text."
  (let* ((response (consult-gh--api-get-json url))
         (content (if (eq (car response) 0) (consult-gh--json-to-hashtable (cadr response) :content)
                    nil)))
    (if content
        (base64-decode-string content)
      "")))

(defun consult-gh--files-get-content-by-path (repo path &optional ref)
  "Fetch the contents of file at PATH in REPO.

optional argument REF is a stringfor branch name, tag name or commit sha"
  (let* ((url (concat (format "repos/%s/contents/%s" repo path) (if ref (format "?ref=%s" (substring-no-properties ref)))))
         (response (consult-gh--api-get-json url))
         (content (if (eq (car response) 0) (consult-gh--json-to-hashtable (cadr response) :content)
                    nil)))
    (if content
        (base64-decode-string content)
      "")))

(defun consult-gh--file-format (cons)
  "Format minibuffer candidates for files.
\(e.g. in `consult-gh--files-read-file'\).

CONS is a list of files for example returned by
`consult-gh--files-nodirectory-items'.
INPUT is the dynamic input in minibuffer.
If HIGHLIGHT is non-nil, highlights the input in candidates."
  (let* ((class "file")
         (type "file")
         (name (car cons))
         (info (cdr cons))
         (path (plist-get info :path))
         (repo (plist-get info :repo))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (size (plist-get info :size))
         (object-type (plist-get info :object-type))
         (ref (plist-get info :ref))
         (sha (plist-get info :sha))
         (api-url (plist-get info :api-url))
         (mode (pcase (plist-get info :mode)
                 ("100644" "file")
                 ("100755" "exec file")
                 ("040000" "directory")
                 ("160000" "commit")
                 ("120000" "symlink")
                 (_  (plist-get info :mode))))
         (str  (cond
                     ((equal object-type "tree")
                      (file-name-as-directory name))
                     ((equal object-type "blob")
                      name)
                     (t
                      name))))
  (add-text-properties 0 1 (list :repo repo
                                     :user user
                                     :package package
                                     :path (and (stringp path)
                                                (substring-no-properties path))
                                     :api-url api-url
                                     :mode mode
                                     :size size
                                     :ref ref
                                     :sha sha
                                     :class class
                                     :type type
                                     :object-type object-type
                                     :new nil)
                           str)
      str))

(defun consult-gh--file-state ()
  "State function for file candidates in `consult-gh--files-read-file'.

This is passed as STATE to `consult--read' on file candidates
and is used to preview files or do other actions on the file."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (let* ((repo (get-text-property 0 :repo cand))
                    (path (get-text-property 0 :path cand))
                    (ref (or (get-text-property 0 :ref cand) "HEAD"))
                    (api-url (get-text-property 0 :api-url cand))
                    (object-type (get-text-property 0 :object-type cand))
                    (tempdir (expand-file-name (concat repo "/" ref "/")
                                               (or consult-gh--current-tempdir
                                                   (consult-gh--tempdir))))
                    (file-p (equal object-type "blob"))
                    (file-size (and file-p (get-text-property 0 :size cand)))
                    (confirm (if (and file-size (>= file-size
                                                    consult-gh-large-file-warning-threshold))
                                 (yes-or-no-p (format "File is %s Bytes.  Do you really want to load it?" file-size))
                               t))
                    (temp-file (or (cdr (assoc (substring-no-properties (concat repo "/" ref "/" path)) consult-gh--open-files-list)) (expand-file-name path tempdir)))
                    (_ (and file-p confirm (progn
                                             (unless (file-exists-p temp-file)
                                               (make-directory (file-name-directory temp-file) t)
                                               (with-temp-file temp-file
                                                 (insert (consult-gh--files-get-content-by-api-url api-url))
                                                 (set-buffer-file-coding-system 'raw-text)
                                                 (set-buffer-multibyte t)
                                                 (let ((after-save-hook nil))
                                                   (write-file temp-file))))
                                             (add-to-list 'consult-gh--open-files-list `(,(substring-no-properties (concat repo "/" ref "/" path)) . ,temp-file)))))
                    (buffer (or (and file-p confirm (find-file-noselect temp-file t)) nil)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (add-to-list 'consult-gh--open-files-buffers buffer)
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun consult-gh--file-annotate ()
  "Annotate each file candidate for `consult-gh--files-read-file'.

For more info on annotation refer to the manual, particularly
`consult--read' and `consult--read-annotate' documentation."
  (lambda (cand)
    (let* ((size (format "%s Bytes" (or (get-text-property 0 :size cand) 0)))
           (repo (format "%s" (get-text-property 0 :repo cand)))
           (user (car (string-split repo "\/")))
           (package (cadr (string-split repo "\/")))
           (ref (get-text-property 0 :ref cand))
           (ref-str (if (and (stringp ref)
                             (equal (get-text-property 0 :type ref) "sha"))
                        (substring ref 0 6)
                      ref))
           (mode (format "%s" (get-text-property 0 :mode cand)))
           (str (format "%s\s%s"
                        (propertize size 'face 'consult-gh-visibility)
                        (propertize mode 'face 'consult-gh-url)))
           (cand (substring-no-properties cand)))
      (if (and cand str)
        (concat
         (propertize " " 'display '(space :align-to center))
         str
         (concat "\t -- " (propertize user 'face 'consult-gh-user ) "/" (propertize package 'face 'consult-gh-package) "@" (propertize ref-str 'face 'consult-gh-branch)))
      nil))))

(defun consult-gh--file-lookup (selected candidates &rest _)
  "Lookup SELECTED in CANDIDATES list, return original element."
  (let* ((sel (member selected candidates)))
    (if sel
        (car sel)
      selected)))

(defun consult-gh--file-group (cand transform)
  "Group function for file candidate, CAND.

This is passed as GROUP to `consult--read' on file candidates
and is used to group files by repository names.

If TRANSFORM is non-nil, return CAND."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-files-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "File " 98 nil ?-)
       (consult-gh--set-string-width " Path " 8 nil ?-)
       (consult-gh--set-string-width " > Repo " 40 nil ?-))))))

(defun consult-gh--files-browse-url-action (cand)
  "Browse the url for a file candidate, CAND.

This is an internal action function that gets a candidate, CAND,
from `consult-gh-find-file' and opens the url of the file in a browser.

To use this as the default action in `consult-gh-find-file',
set `consult-gh-file-action' to `consult-gh--files-browse-url-action'."
  (let* ((repo (get-text-property 0 :repo cand))
         (path (get-text-property 0 :path cand))
         (ref (get-text-property 0 :ref cand))
         (url (concat (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")) "/blob/" ref "/" path)))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--files-view (repo path api-url &optional no-select tempdir jump-to-str ref revert find-func)
  "Open file in an Emacs buffer.

This is an internal function that gets the PATH to a file within a REPO and
the URL of the file on GitHub API, then fetches the content from GitHub by
`consult-gh--files-get-content-by-api-url' and inserts it into a temporary
file stored under `consult-gh-tempdir' in appropriate subdirectories for REPO.

If the optional input NO-SELECT is nil, it switches to the buffer
by `find-file', otherwise it does not swith-to-buffer and only returns the
name of the buffer.

To use this as the default action in `consult-gh-find-file',
see `consult-gh--files-view-action'.

Description of Arguments:
  REPO      full name of the repo e.g. “arimindarvish/consult-gh”
  PATH      the relative path of the file to the root of repo
            e.g “./README.org”
  API-URL   the url of the file as retrieved from GitHub API
  NO-SELECT a boolean for whether to swith-to-buffer or not
  TEMPDIR   the directory where the temporary file is saved
  REF       a string; branch name, tag name or commit sha
  REVERT    if non-nil, pull the file from remote
  FIND-FUNC function to use instead of `find-file'
            (this is useful for example for opening file in other window)

Output is the buffer visiting the file."
  (let* ((tempdir (or tempdir consult-gh--current-tempdir (consult-gh--tempdir)))
         (temp-file (or (cdr (assoc (substring-no-properties (concat repo "/" ref "/" path)) consult-gh--open-files-list)) (expand-file-name path tempdir)))
         (topic (format "%s/%s" repo path)))
    (add-text-properties 0 1 (list :repo repo :type "file" :path path :ref ref :title nil :api-url api-url :changed-locally nil) topic)
    (unless (file-exists-p temp-file)
      (make-directory (file-name-directory temp-file) t)
      (with-temp-file temp-file
        (insert (if api-url (consult-gh--files-get-content-by-api-url api-url)
                  (consult-gh--files-get-content-by-path repo path ref)))
        (set-buffer-file-coding-system 'raw-text)
        (set-buffer-multibyte t)
        (let ((after-save-hook nil))
          (write-file temp-file)))
      (add-to-list 'consult-gh--open-files-list `(,(substring-no-properties (concat repo "/" ref "/" path)) . ,temp-file)))
    (if no-select
        (find-file-noselect temp-file)
      (with-current-buffer (funcall (or find-func #'find-file) temp-file)
        (if (or revert
                (and (stringp consult-gh--topic)
                     (get-text-property 0 :changed-locally consult-gh--topic)
                     (y-or-n-p "The file has changed locally in this buffer.  Do you want to revert the buffer form remote? ")))
            (let ((inhibit-read-only t))
              (save-excursion
                (erase-buffer)
                (insert (if api-url (consult-gh--files-get-content-by-api-url api-url)
                          (consult-gh--files-get-content-by-path repo path ref)))
                (set-buffer-file-coding-system 'raw-text)
                (set-buffer-multibyte t)
                (setq-local consult-gh--topic topic)
                (let ((after-save-hook nil))
                  (write-file temp-file)))))
        (if jump-to-str
            (progn
              (goto-char (point-min))
              (search-forward jump-to-str nil t)
              (consult-gh-recenter 'middle))
          nil)
        (add-to-list 'consult-gh--preview-buffers-list (current-buffer))
        (add-to-list 'consult-gh--open-files-buffers (current-buffer))
        (consult-gh-file-view-mode +1)
        (unless (stringp consult-gh--topic)
          (setq-local consult-gh--topic topic))
        (add-text-properties 0 1 (list :view-buffer (current-buffer)) consult-gh--topic)
        (current-buffer)))))

(defun consult-gh--files-view-action (cand)
  "Open file candidate, CAND, in an Emacs buffer.

This is a wrapper function around `consult-gh--files-view'.

It parses CAND to extract relevant values
\(e.g. repository, file path, ...\) and passes them to
`consult-gh--files-view'.

To use this as the default action on consult-gh's files,
set `consult-gh-file-action' to `consult-gh--files-view-action'."
  (save-match-data
    (let* ((repo (get-text-property 0 :repo cand))
           (path (get-text-property 0 :path cand))
           (api-url (get-text-property 0 :api-url cand))
           (ref (or (get-text-property 0 :ref cand) "HEAD"))
           (object-type (get-text-property 0 :object-type cand))
           (mode (get-text-property 0 :mode cand))
           (tempdir (expand-file-name (concat repo "/" ref "/")
                                      (or consult-gh--current-tempdir (consult-gh--tempdir))))
           (file-p (and
                    (equal object-type "blob")
                    (or (equal mode "file")
                        (equal mode "symlink"))))
           (file-size (and file-p (get-text-property 0 :size cand)))
           (confirm t))
      (pcase mode
        ((or "file" "symlink")
         (if file-size
             (when (>= file-size consult-gh-large-file-warning-threshold)
               (if (yes-or-no-p (format "File is %s Bytes.  Do you really want to load it?" file-size))
               (setq confirm t)
             (setq confirm nil))))
         (if (and file-p confirm)
             (consult-gh--files-view repo path api-url nil tempdir nil ref)))
        ("directory"
         (if consult-gh-files-use-dired-like-mode
             (consult-gh-dired repo (or (and (stringp path)
                                             (file-name-as-directory path))
                                        path)
                               ref)))
        ("commit"
         (let* ((info (consult-gh--api-get-command-string (concat (format "repos/%s/contents/%s" repo path) (if ref (format "?ref=%s" ref)))))
                (type (consult-gh--json-to-hashtable info :type)))
           (if (equal type "submodule")
               (let* ((git-url (consult-gh--json-to-hashtable info :submodule_git_url))
                      (module-sha (consult-gh--json-to-hashtable info :sha))
                      (module-sha (and (stringp module-sha) (propertize module-sha :type "sha")))
                      (urlparsed (url-generic-parse-url git-url))
                      (urlhost (and urlparsed (url-host urlparsed)))
                      (urlpath (car-safe (and urlparsed (url-path-and-query urlparsed)))))
                 (cond
                  ((stringp urlhost)
                   (cond
                    ((string-match-p ".*github.*" urlhost)
                     (consult-gh-find-file (string-remove-suffix ".git" (string-remove-prefix "/" urlpath)) module-sha))
                    (t
                      (y-or-n-p (format "Cannot open that submodule in consult-gh.  Do you want to browse the link:  %s? " git-url))
                      (funcall (or consult-gh-browse-url-func #'browse-url) git-url))))
                  ((and (stringp urlpath)
                        (string-match-p "git@.*" urlpath))
                   (cond
                    ((string-match "git@.*github.com:\\(?1:.*\\)" urlpath)
                    (consult-gh-find-file (string-remove-prefix "/" (string-remove-suffix ".git" (match-string 1 urlpath))) module-sha))
                    ((string-match "git@.*gitlab.com:\\(?1:.*\\)" urlpath)
                     (let ((submodule-link (format "https://gitlab.com/%s" (match-string 1 urlpath))))
                        (and (y-or-n-p (format "Cannot open that submodule in consult-gh.  Do you want to open the link:  %s in the browser? " (propertize submodule-link 'face 'consult-gh-warning)))
                         (funcall (or consult-gh-browse-url-func #'browse-url) submodule-link))))
                    (t
                      (message "Do not know how to open the submodule: %s" git-url)))))))))))))

(defun consult-gh--files-save-file (repo path targetpath &optional ref api-url )
  "Save fileat REPO and PATH to TARGETPATH.

Optional argument REF is name of a branch or tag.
When API-URL is non-nil, it is used to get file contents
instead of repo and path.  This is useful for example for
getting contents of files in a specific commit."
  (let* ((ref (or ref "HEAD"))
         (save-path (file-truename targetpath))
         (save-dir (and (stringp save-path) (file-name-directory save-path)))
         (file-buff (consult-gh--files-view repo path api-url t nil nil ref t)))
    (when save-dir
      (unless (file-exists-p save-dir)
              (make-directory save-dir t)))
    (with-current-buffer file-buff
      (let ((after-save-hook nil))
        (write-file save-path t)
        save-path))))

(defun consult-gh--files-save-file-action (cand)
  "Save file candidate, CAND, to a file.

Its parses CAND to extract relevant information
\(e.g. repository name, file path, ...\)
and passes them to `consult-gh--files-save-file',

If `consult-gh-ask-for-path-before-save' is non-nil,
it queries the user for a file path, otherwise it saves the file under
`consult-gh-default-save-directory' with the variable `buffer-file-name' as
the name of the file.

To use this as the default action on consult-gh's files,
set `consult-gh-file-action' to `consult-gh--files-save-file-action'."
  (let* ((repo (get-text-property 0 :repo cand))
         (path (get-text-property 0 :path cand))
         (api-url (get-text-property 0 :api-url cand))
         (ref (get-text-property 0 :ref cand))
         (object-type (get-text-property 0 :object-type cand))
         (file-p (equal object-type "blob"))
         (file-size (and file-p (get-text-property 0 :size cand)))
         (filename (and file-p (file-name-nondirectory path)))
         (targetpath (if consult-gh-ask-for-path-before-save
                         (file-truename (read-file-name "Save As: " consult-gh-default-save-directory nil nil filename))
                       (expand-file-name filename consult-gh-default-save-directory)))
         (confirm t))
    (when (and file-size (>= file-size consult-gh-large-file-warning-threshold))
      (if (yes-or-no-p (format "File is %s Bytes.  Do you really want to load it?" file-size))
          (setq confirm t)
        (setq confirm nil)))
      (if (and file-p confirm)
          (save-mark-and-excursion
            (save-restriction
              (consult-gh--files-save-file repo path targetpath ref api-url))))))

(defun consult-gh--files-edit-presubmit (&optional file)
  "Prepare edits on FILE to submit.

FILE is a string with properties that identify a github file.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--files-view'."
  (if consult-gh-file-view-mode
      (let* ((file (or file
                       (and (stringp consult-gh--topic)
                            (equal (get-text-property 0 :type consult-gh--topic) "file")
                            consult-gh--topic)))
             (repo (get-text-property 0 :repo file))
             (ref (get-text-property 0 :ref file))
             (files (cond
                     ((listp file) file)
                     ((stringp file)
                      (list file))))
             (nextsteps (append (list (cons "Create a Commit and Push Changes Directly to GitHub" :submit))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))

        (if files
          (pcase next
            (':submit
               (consult-gh--create-commit files repo ref)))
          (message "No file to edit!")))
    (message "Not a Github file buffer!")))

(defun consult-gh--files-edit-commit-changes (&optional file)
  "Commit the edits in FILE.

FILE is a string with properties that identify a github file.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--files-view'."
  (let* ((file (or file
                   (and (stringp consult-gh--topic)
                        (equal (get-text-property 0 :type consult-gh--topic) "file")
                        consult-gh--topic)))
         (ref (and (stringp file)
                   (get-text-property 0 :ref file)))
         (repo (get-text-property 0 :repo file))
         (commit-buffer (get-text-property 0 :commit-buffer file)))

    (when (stringp ref)
      (cond
       ((equal (get-text-property 0 :type ref) "sha")
        (user-error "Cannot edit files in a commit ref"))
       ((equal (get-text-property 0 :type ref) "tag")
        (user-error "Cannot edit files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))


    (when file
      (if (and (bufferp commit-buffer)
               (buffer-live-p commit-buffer))
          (with-current-buffer commit-buffer
            (let* ((files (get-text-property 0 :files consult-gh--topic))
                   (new-files  (cl-remove-duplicates (append (list file) files) :test #'equal)))

              (add-text-properties 0 1 (list :files new-files) consult-gh--topic)

              (save-excursion
                (when-let* ((regions (consult-gh--get-region-with-prop ':consult-gh-commit-instructions))
                           (goto-char (car-safe (car-safe regions)))))
                (consult-gh--delete-region-with-prop ':consult-gh-commit-instructions)
                (insert (propertize consult-gh-commit-message-instructions :consult-gh-commit-instructions t))
                (when (listp new-files)
                  (insert (propertize "\n\n" :consult-gh-commit-instructions t))
                  (insert (propertize "# Files to Create/Update:" :consult-gh-commit-instructions t))
                  (mapc (lambda (f)
                          (consult-gh--commit-create-make-diff-buffer repo f ref)
                          (add-text-properties 0 1 (list :commit-buffer commit-buffer) f)

(insert (propertize (format "\n# - create/update %s" f) :consult-gh-commit-instructions t)))
                        new-files)))

              (funcall consult-gh-pop-to-buffer-func commit-buffer)))
        (consult-gh--files-edit-presubmit file)))))

(defun consult-gh--files-edit-save-buffer-hook (&rest _args)
  "Hook for commiting to GitHub after `save-buffer'."
    (when consult-gh-file-view-mode
    (add-text-properties 0 1 (list :changed-locally t) consult-gh--topic)
      (consult-gh--files-edit-commit-changes)))

(defun consult-gh--files-create-state ()
  "State function for previewing existing file.

This is passed as STATE to a `consult--read' in `consult-gh-create-file'
and is used to preview existing files."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview (listp cand))
             (let* ((info (cdr cand))
                    (path (plist-get info :path)))
               (cond
                ((stringp path)
                 (let* ((repo (plist-get info :repo))
                        (ref (or (plist-get info :ref) "HEAD"))
                        (size (plist-get info :size))
                        (object-type (plist-get info :object-type))
                        (api-url (plist-get info :api-url))
                        (tempdir (expand-file-name (concat repo "/" ref "/")
                                                   (or consult-gh--current-tempdir
                                                       (consult-gh--tempdir))))
                        (file-p (equal object-type "blob"))
                        (file-size (and file-p size))
                        (confirm (if (and file-size (>= file-size
                                                        consult-gh-large-file-warning-threshold))
                                     (yes-or-no-p (format "File is %s Bytes.  Do you really want to load it?" file-size))
                                   t))
                        (temp-file (or (cdr (assoc (substring-no-properties (concat repo "/" ref "/" path)) consult-gh--open-files-list)) (expand-file-name path tempdir)))
                        (_ (and file-p confirm (progn
                                                 (unless (file-exists-p temp-file)
                                                   (make-directory (file-name-directory temp-file) t)
                                                   (with-temp-file temp-file
                                                     (insert (if api-url (consult-gh--files-get-content-by-api-url api-url)
                                                               (consult-gh--files-get-content-by-path repo path ref)))
                                                     (set-buffer-file-coding-system 'raw-text)
                                                     (set-buffer-multibyte t)
                                                     (let ((after-save-hook nil))
                                                       (write-file temp-file))
                                                     (set-buffer-modified-p nil)))
                                                 (add-to-list 'consult-gh--open-files-list `(,(substring-no-properties (concat repo "/" ref "/" path)) . ,temp-file)))))
                        (buffer (or (and file-p confirm (find-file-noselect temp-file t)) nil)))
                   (add-to-list 'consult-gh--preview-buffers-list buffer)
                   (add-to-list 'consult-gh--open-files-buffers buffer)
                   (funcall preview action
                            buffer)))))))
        ('return
         cand)))))

(defun consult-gh--files-create-buffer (repo path &optional ref content tempdir)
  "Create file buffer for new file at PATH in REF of REPO.

Description of Arguments:

  REPO    a string; name of the repository
  PATH    a string; path of file to create in repo
  REF     a string; branch name
  CONTENT a string; initial content of the file
  TEMPDIR a string; temp directory to save the local file."
  (let* ((tempdir (or tempdir consult-gh--current-tempdir (consult-gh--tempdir)))
         (temp-file (or (cdr (assoc (substring-no-properties (concat repo "/" ref "/" path)) consult-gh--open-files-list)) (expand-file-name path tempdir)))
         (topic (format "%s/%s" repo path)))
    (add-text-properties 0 1 (list :repo repo :type "file" :path path :ref ref :sha nil :title nil :api-url nil :changed-locally nil :new t :object-type "blob") topic)
    (unless (file-exists-p temp-file)
      (make-directory (file-name-directory temp-file) t)
      (add-to-list 'consult-gh--open-files-list `(,(substring-no-properties (concat repo "/" ref "/" path)) . ,temp-file)))
    (with-current-buffer (find-file temp-file)
        (add-to-list 'consult-gh--preview-buffers-list (current-buffer))
        (add-to-list 'consult-gh--open-files-buffers (current-buffer))
        (consult-gh-file-view-mode +1)
        (read-only-mode -1)
        (setq-local consult-gh--topic topic)
        (add-text-properties 0 1 (list :view-buffer (current-buffer) :local-path (buffer-file-name (current-buffer))) consult-gh--topic)
        (when (and content (stringp content))
          (erase-buffer)
          (insert content)
          (set-buffer-modified-p nil))
        (goto-char (point-min))
        (current-buffer))))

(defun consult-gh--files-delete (repo files &optional ref commit-message)
  "Delete the list of files at FILES in REF of REPO.

PATHS is a list of strings."
  (let* ((ref (or ref "HEAD")))
    (consult-gh--delete-commit repo files ref commit-message)))

(defun consult-gh--files-read-upload-targets ()
"Query user to pick the target path for uploading files."
(and consult-gh--upload-targets
     (listp consult-gh--upload-targets)
     (consult--read (mapcar (lambda (item)
                              (let* ((title (get-text-property 0 :title item))
                                     (repo (get-text-property 0 :repo item))
                                     (path (get-text-property 0 :path item))
                                     (ref (get-text-property 0 :ref item))
                                     (topic (concat title repo "\t" ref "\t" path)))
                                (add-text-properties 0 1 (text-properties-at 0 item) topic)
                              topic))
                            (append (list (propertize "Select a new one interactively" :title "Select a new one interactively"))
                            consult-gh--upload-targets))
                    :prompt "Select The Target Repo, Branch and Path: "
                    :lookup #'consult--lookup-member
                    :require-match t
                    :sort nil)))

(defun consult-gh--files-upload-marked-dired-files ()
"Get files to upload to GitHUb from Dired."
(require 'dired nil t)
(if (derived-mode-p 'dired-mode)
    (nreverse (dired-map-over-marks (dired-get-filename) nil))))

(defun consult-gh--files-upload-dired (topic &optional files)
"Open Dired to select FILES to upload.

TOPIC is a string with propertis that describe repo and path
for uploading files."
(let* ((repo (get-text-property 0 :repo topic))
       (path (get-text-property 0 :path topic)))
    (with-current-buffer (dired-noselect (or files (car-safe (dired-read-dir-and-switches ""))))
      (add-text-properties 0 1 (list :dired-buffer (current-buffer)) topic)
      (setq-local consult-gh--upload-topic topic)
      (add-to-list 'consult-gh--upload-targets topic)
      (consult-gh-upload-files-mode +1)
      (setq-local header-line-format (concat "Mark files to upload to "
                                                     (consult-gh--get-package repo)
                                                     (concat "/" path)
                                            ".  "
                                            (substitute-command-keys "When done, use `\\[consult-gh-upload-files]' to submit or `\\[consult-gh-topics-cancel]' to cancel.")))
      (funcall consult-gh-pop-to-buffer-func (current-buffer)))))

(defun consult-gh--files-rename (repo files &optional ref commit-message)
  "Rename the list of FILES in REF of REPO.

FILES is a list of propertized strings with
old-path and new-path in properties."
  (let* ((ref (or ref "HEAD")))
    (consult-gh--rename-commit files repo ref commit-message)))

(defun consult-gh--repo-format (string input highlight)
  "Format minibuffer candidates for repos in `consult-gh-search-repos'.

Description of Arguments:

  STRING    output of a “gh” call \(e.g. “gh search repos ...”\).
  INPUT     a query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted with
            `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "repo")
         (type "repo")
         (parts (string-split string "\t"))
         (repo (car parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (description (cadr parts))
         (visibility (cadr (cdr parts)))
         (date (cadr (cdr (cdr parts))))
         (date (if (length> date 9) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s"
                      (concat
                       (and user (propertize user 'face 'consult-gh-user))
                       (and package "/")
                       (and package (propertize package 'face 'consult-gh-package)))
                      (consult-gh--justify-left (propertize visibility 'face 'consult-gh-visibility) repo (frame-width))
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize description 'face 'consult-gh-description))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :package package :description description :visibility visibility :date date :query query :class class :type type) str)
    str))

(defun consult-gh--repo-state ()
  "State function for repo candidates.

This is passed as STATE to `consult--read'
in `consult-gh-search-repos' and is used
to preview or do other actions on the repo."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (query (get-text-property 0 :query cand))
                        (match-str (consult--build-args query))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (consult-gh--repo-view (format "%s" repo) buffer)
               (with-current-buffer buffer
                 (if consult-gh-highlight-matches
                     (cond
                      ((listp match-str)
                       (mapc (lambda (item)
                                 (highlight-regexp item 'consult-gh-preview-match)) match-str))
                      ((stringp match-str)
                       (highlight-regexp match-str 'consult-gh-preview-match)))))
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun consult-gh--repo-group (cand transform)
  "Group function for repo candidate, CAND.

This is passed as GROUP to `consult--read'
in `consult-gh-search-repos' and is used
to group repos by user\owner's names.

If TRANSFORM is non-nil, return the CAND itself."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-repos-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat "Repository  "
              (consult-gh--justify-left " Visibility " "Repository  " (frame-width) ?-)
              (consult-gh--set-string-width " Date " 12 nil ?-)
              " Description")))))

(defun consult-gh--repo-browse-url-action (cand)
  "Browse the url for a repo candidate, CAND.

This is an internal action function that gets a candidate, CAND,
for example from `consult-gh-search-repos' and opens the url of the repo
in an external browser.

To use this as the default action for repos,
set `consult-gh-repo-action' to `consult-gh--repo-browse-url-action'."
  (let* ((repo (get-text-property 0 :repo cand))
         (response (consult-gh--call-process "browse" "--repo" (substring-no-properties repo) "--no-browser"))
         (url (string-trim (cadr response))))
    (if (eq (car response) 0)
        (funcall (or consult-gh-browse-url-func #'browse-url) url)
      (message url))))

(defun consult-gh--repo-insert-readme-gfm (repo &optional topic)
  "Insert REPO's Readme in GitHub flavor markdown format at point.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example,
see the buffer-local variable `consult-gh--topic' in the buffer created
by `consult-gh--repo-view'."
  (let* ((topic (or topic consult-gh--topic))
         (readme (consult-gh--command-to-string "repo" "view" repo))
         (info  (consult-gh--api-get-command-string (format "repos/%s/readme" repo)))
         (path (consult-gh--json-to-hashtable info :path))
         (size (consult-gh--json-to-hashtable info :size))
         (api-url (consult-gh--json-to-hashtable info :url)))
    (add-text-properties 0 1 (list :readme-path path :readme-size size :readme-api-url api-url) topic)
    (when (stringp readme)
      (save-mark-and-excursion
        (insert readme)
        (set-buffer-modified-p nil)
        (gfm-mode)
        (when (display-images-p)
          (markdown-display-inline-images))
        (set-buffer-modified-p nil))
      nil)))

(defun consult-gh--repo-insert-readme-markdown (repo &optional topic)
  "Insert REPO's Readme in markdown format at point.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example,
see the buffer-local variable `consult-gh--topic' in the buffer created
by `consult-gh--repo-view'."
  (let* ((topic (or topic consult-gh--topic))
         (readme (consult-gh--command-to-string "repo" "view" repo))
         (info  (consult-gh--api-get-command-string (format "repos/%s/readme" repo)))
         (path (consult-gh--json-to-hashtable info :path))
         (size (consult-gh--json-to-hashtable info :size))
         (api-url (consult-gh--json-to-hashtable info :url)))
    (add-text-properties 0 1 (list :readme-path path :readme-size size :readme-api-url api-url) topic)
    (when (stringp readme)
      (save-mark-and-excursion
        (insert readme)
        (set-buffer-modified-p nil)
        (markdown-mode)
        (when (display-images-p)
          (markdown-display-inline-images))
        (set-buffer-modified-p nil))
      nil)))

(defun consult-gh--repo-insert-readme-org (repo &optional topic)
  "Insert REPO's Readme in org format at point.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example,
see the buffer-local variable `consult-gh--topic' in the buffer created
by `consult-gh--repo-view'."
  (let* ((topic (or topic consult-gh--topic))
         (org-display-remote-inline-images 'download)
         (info (cadr (consult-gh--api-get-json (format "repos/%s" repo))))
         (name (consult-gh--json-to-hashtable info :name))
         (desc (consult-gh--json-to-hashtable info :description))
         (readme (consult-gh--api-get-command-string (format "repos/%s/readme" repo)))
         (path (consult-gh--json-to-hashtable readme :path))
         (size (consult-gh--json-to-hashtable readme :size))
         (api-url (consult-gh--json-to-hashtable readme :url))
         (extension (and (stringp path) (file-name-extension path)))
         (content (consult-gh--json-to-hashtable readme :content)))
    (add-text-properties 0 1 (list :readme-path path :readme-size size :readme-api-url api-url) topic)
    (save-mark-and-excursion
      (insert ""
              "#+name:\t" (or name "") "\n"
              "#+description:\t" (or desc "") "\n"
              (make-string 5 ?\-)
              "\n\n")
      (when content
        (insert (base64-decode-string content))
        (set-buffer-file-coding-system 'raw-text)
        (set-buffer-multibyte t))
      (cond
       ((and (stringp extension) (equal (downcase extension) "org"))
        (org-mode)
        (org-table-map-tables 'org-table-align t)
        (org-fold-show-all))
       ((and (stringp extension) (member (downcase extension) '("md" "mkdn" "mdown" "markdown")))
        (consult-gh--github-header-to-org)
        (consult-gh--markdown-to-org))
       (t
        (org-mode)))
      (set-buffer-modified-p nil)))
  nil)

(defun consult-gh--repo-insert-readme (repo &optional topic)
  "Insert REPO's Readme at point.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example,
see the buffer-local variable `consult-gh--topic' in the buffer created
by `consult-gh--repo-view'."
  (let* ((topic (or topic consult-gh--topic))
         (info (cadr (consult-gh--api-get-json (format "repos/%s" repo))))
         (name (consult-gh--json-to-hashtable info :name))
         (desc (consult-gh--json-to-hashtable info :description))
         (readme (cadr (consult-gh--api-get-json (format "repos/%s/readme" repo))))
         (path (consult-gh--json-to-hashtable readme :path))
         (size (consult-gh--json-to-hashtable readme :size))
         (api-url (consult-gh--json-to-hashtable readme :url))
         (content (consult-gh--json-to-hashtable readme :content)))

    (add-text-properties 0 1 (list :readme-path path :readme-size size :readme-api-url api-url) topic)

    (save-mark-and-excursion
      (setq-local buffer-file-name path)
      (insert ""
              (or comment-start "") "name:\t" (or name "") (or comment-end "") "\n"
              (or comment-start "") "description:\t" (or desc "") (or comment-end "") "\n"
              (make-string 5 ?\-)
              "\n\n")
      (when content
        (insert (base64-decode-string content))
        (set-buffer-file-coding-system 'raw-text)
        (set-buffer-multibyte t))
      (normal-mode)
      (set-buffer-modified-p nil)))
  nil)

(defun consult-gh--repo-view (repo &optional buffer preview)
  "Open REPO's Readme in an Emacs buffer, BUFFER.

This is an internal function that takes REPO, the full name of
a GitHub repository \(e.g. “armindarvish/consult-gh”\) and
shows the README of that repo in an Emacs buffer.

It fetches the preview from GitHub by “gh repo view REPO”
and puts the response as raw text in the buffer defined by
the optional input, BUFFER.  If BUFFER is nil, a buffer named by
`consult-gh-preview-buffer-name' is used instead.

If `consult-gh-repo-preview-major-mode' is non-nil, uses it to set the
major-mode, otherwise uses the major mode associated with the README's
file extension (e.g. .md, .org, .rst).

Description of Arguments:

REPO    a string; the name of the repository to be previewed.
BUFFER  a string; an optional buffer the preview should be shown in.
PREVIEW a boolean; when non-nil loads the preview without details."
  (with-current-buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name))
    (let* ((inhibit-read-only t)
           (info (consult-gh--api-get-command-string (format "/repos/%s" repo)))
           (default-branch (consult-gh--json-to-hashtable info :default_branch))
           (open-issues-count (consult-gh--json-to-hashtable info :open_issue_count))
           (html-url (consult-gh--json-to-hashtable info :html_url))
          (topic (format "%s" repo)))

      (add-text-properties 0 1 (list :repo repo :type "repo" :title repo :default-branch default-branch :open-issues-count open-issues-count :url html-url) topic)

      (unless preview
      (consult-gh--completion-set-all-fields repo topic (consult-gh--user-canwrite repo)))

      (erase-buffer)
      (pcase consult-gh-repo-preview-major-mode
        ('gfm-mode
         (consult-gh--repo-insert-readme-gfm repo topic))
        ('markdown-mode
         (consult-gh--repo-insert-readme-markdown repo topic))
        ('org-mode
         (consult-gh--repo-insert-readme-org repo topic))
        (_ (consult-gh--repo-insert-readme repo topic)))
      (goto-char (point-min))
      (consult-gh-repo-view-mode +1)
      (setq-local consult-gh--topic topic)
      (current-buffer))))

(defun consult-gh--repo-view-action (cand)
  "Open the preview of a repo candidate, CAND.

This is a wrapper function around `consult-gh--repo-view'.
It parses CAND to extract relevant values \(e.g. repository's name\) and
passes them to `consult-gh--repo-view'.

To use this as the default action for repos, set
`consult-gh-repo-action' to function `consult-gh--repo-view-action'."

  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the README in the existing buffer." :replace)
                             (cons "Make a new buffer and load the README in it (without killing the old buffer)." :new))
                       :prompt "You already have this repo open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))
    (if existing
        (cond
         ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
         ((eq confirm :replace)
          (message "Reloading README in the existing buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--repo-view repo existing)))
         ((eq confirm :new)
          (message "Opening README in a new buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--repo-view repo (generate-new-buffer buffername nil)))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--repo-view repo))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--repo-browse-files-action (cand)
  "Browse file tree of a repo candidate, CAND.

Opens the preview of a repo candidate, CAND, in an Emacs buffer.

This is a wrapper function around `consult-gh-find-file'.
It parses CAND to extract relevant values \(e.g. repository name\)
and passes them to `consult-gh-find-file'.

To use this as the default action for repos,
set `consult-gh-repo-action' to `consult-gh--repo-browse-files-action'."
  (let* ((repo (get-text-property 0 :repo cand)))
    (consult-gh-find-file repo)))

(defvar consult-gh-repo-post-clone-hook nil
  "Functions called after `consult-gh--repo-clone'.

Full path of the cloned repo is passed to these functions.")

(defun consult-gh--repo-clone (repo name targetdir &optional extra-args &rest _)
  "Clones REPO to the path TARGETDIR/NAME.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-repo-clone'.

It runs the command “gh clone REPO TARGETDIR/NAME”
using `consult-gh--command-to-string'.

EXTRA-ARGS are passed to “gh repo clone”."
  (let* ((buffer (current-buffer))
        (extra-args (cond
               ((stringp extra-args)
                  (list extra-args))
               ((listp extra-args)
                extra-args)))
        (cmd-args (append (list "repo" "clone" (format "%s" repo) (expand-file-name name targetdir))
                          extra-args)))

    (consult-gh--make-process (format "consult-gh-clone-%s" repo)
                            :when-done (lambda (_event _out)
                                          (with-current-buffer buffer
                                            (progn
                                            (run-hook-with-args 'consult-gh-repo-post-clone-hook (expand-file-name name targetdir))
                                            (message "repo %s was cloned to %s" (propertize repo 'face 'font-lock-keyword-face) (propertize (expand-file-name name targetdir) 'face 'font-lock-type-face)))))
                            :cmd-args cmd-args))
    (let ((inhibit-message t))
       (propertize (expand-file-name name targetdir) :origin repo)))

(defun consult-gh--repo-clone-action (cand)
  "Clones a repo candidate, CAND.

This is a wrapper function around `consult-gh--repo-clone'.
It parses CAND to extract relevant values \(e.g. repository's name\)
and passes them to `consult-gh--repo-clone'.

To use this as the default action for repos,
set `consult-gh-repo-action' to `consult-gh--repo-clone-action'.

If `consult-gh-confirm-before-clone' is nil it clones the repo
under `consult-gh-default-clone-directory' and uses the package name
from REPO as the default name for the cloned folder."

  (let* ((reponame (get-text-property 0 :repo cand))
         (package (consult-gh--get-package reponame)))
    (if consult-gh-confirm-before-clone
        (let* ((targetdir (read-directory-name (concat "Select Directory for " (propertize (format "%s: " reponame) 'face 'font-lock-keyword-face)) (or (and (stringp consult-gh-default-clone-directory) (file-name-as-directory consult-gh-default-clone-directory)) default-directory)))
               (name (read-string "name: " package)))
          (consult-gh--repo-clone reponame name targetdir))
      (consult-gh--repo-clone reponame package consult-gh-default-clone-directory))))

(defvar consult-gh-repo-post-fork-hook nil
  "Functions called after `consult-gh--repo-fork'.

Full name of the forked repo e.g. “armindarvish/consult-gh”
is passed to these functions as input arg.")

(defun consult-gh--repo-fork (repo &optional name)
  "Fork REPO as NAME.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-repo-fork'.

It runs the command “gh fork REPO --fork-name NAME”
using `consult-gh--command-to-string'."
  (let* ((package (consult-gh--get-package repo))
         (name (or name package))
         (forkrepo (concat (consult-gh--get-current-username) "/" name)))
    (consult-gh--make-process (format "consult-gh-fork-%s" repo)
                              :when-done (lambda (_proc _str)
                                            (run-hook-with-args 'consult-gh-repo-post-fork-hook forkrepo)
                                            (message "repo %s was forked to %s" (propertize repo 'face 'font-lock-keyword-face) (propertize forkrepo 'face 'font-lock-warning-face)))
                              :cmd-args (list "repo" "fork" (format "%s" repo) "--fork-name" name))
    (let ((inhibit-message t))
      forkrepo)))

(defun consult-gh--repo-fork-action (cand)
  "Fork a repo candidate, CAND.

This is a wrapper function around `consult-gh--repo-fork'.
It parses CAND to extract relevant values \(e.g. repository name\)
and passes them to `consult-gh--repo-fork'.

To use this as the default action for repos,
set `consult-gh-repo-action' to `consult-gh--repo-fork-action'."
  (let* ((reponame (get-text-property 0 :repo cand)))
    (consult-gh--repo-fork reponame)))

(defun consult-gh--repo-delete (repo &optional noconfirm)
"Delete REPO.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-repo-delete'.

It runs the command “gh repo delte REPO”
using `consult-gh--command-to-string'.

When NOCONFIRM is non-nil, does not ask for confirmation."
    (unless noconfirm
      (let ((try 1)
            (repo-confirm (read-string (format "Type %s to confirm deleting repo: " (propertize repo 'face 'consult-gh-repo)))))
      (while (and (not (equal repo repo-confirm)) (< try 3))
                        (setq try (1+ try))
                        (setq repo-confirm (read-string (format "Try %s. Try again and type %s to confirm deleting repo: " (propertize (format "%s/3" try) 'face 'consult-gh-warning) (propertize repo 'face 'consult-gh-repo)))))
      (if (not (equal repo repo-confirm))
               (message "Did not get confirmation in 3 trials. Canceled!")
        (setq noconfirm (equal repo repo-confirm)))))
    (if noconfirm
        (progn
          (consult-gh--command-to-string "repo" "delete" repo "--yes")
          (message "repo %s was %s" (propertize repo 'face 'consult-gh-repo) (propertize "DELETED!" 'face 'consult-gh-warning)))))

(defun consult-gh--repo-delete-action (cand)
  "Delete a repo candidate, CAND.

This is a wrapper function around `consult-gh--repo-delete'.
It parses CAND to extract relevant values \(e.g. repository's name\)
and passes them to `consult-gh--repo-delete'.

To use this as the default action for repos,
set `consult-gh-repo-action' to `consult-gh--repo-delete-action'.

If `consult-gh-confirm-before-delete-repo' is non-nil it asks for confirmation
before deleting the repo, otherise deletes the repo without asking for
confirmation."

  (let* ((reponame (get-text-property 0 :repo cand)))
    (if consult-gh-confirm-before-delete-repo
        (consult-gh--repo-delete reponame)
      (consult-gh--repo-delete reponame t))))

(defun consult-gh--repo-rename (repo &optional new-name noconfirm)
"Rename REPO to NEW-NAME.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-repo-rename'.

It runs the command “gh repo rename --repo REPO”
using `consult-gh--command-to-string'.

When NOCONFIRM is non-nil, does not ask for confirmation."
    (let*  ((new-name (or new-name (read-string "Enter the new name: "))))

(unless noconfirm
      (let ((try 1)
            (repo-confirm (read-string (format "Type %s to confirm renaming repo: " (propertize new-name 'face 'consult-gh-repo)))))
      (while (and (not (equal new-name repo-confirm)) (< try 3))
                        (setq try (1+ try))
                        (setq repo-confirm (read-string (format "Try %s. Try again and type %s to confirm renaming repo: " (propertize (format "%s/3" try) 'face 'consult-gh-warning) (propertize new-name 'face 'consult-gh-repo)))))
      (if (not (equal new-name repo-confirm))
               (message "Did not get confirmation in 3 trials. Canceled!")
        (setq noconfirm (equal new-name repo-confirm)))))
    (if noconfirm
        (progn
          (consult-gh--command-to-string "repo" "rename" "--repo" repo new-name "--yes")
          (message "repo %s was %s to %s"
                   (propertize repo 'face 'consult-gh-repo)
                   (propertize "renamed" 'face 'consult-gh-success)
                   (propertize new-name 'face 'consult-gh-warning))))))

(defun consult-gh--repo-rename-action (cand)
  "Rename a repo candidate, CAND.

This is a wrapper function around `consult-gh--repo-rename'.
It parses CAND to extract relevant values \(e.g. repository's name\)
and passes them to `consult-gh--repo-rename'.

To use this as the default action for repos, set
`consult-gh-repo-action' to `consult-gh--repo-rename-action'.

If `consult-gh-confirm-before-rename-repo' is non-nil it asks for
confirmation before deleting the repo, otherise deletes the repo
without asking for confirmation."
  (let* ((reponame (get-text-property 0 :repo cand)))
    (if consult-gh-confirm-before-rename-repo
        (consult-gh--repo-rename reponame)
      (consult-gh--repo-rename reponame t))))

(defun consult-gh--repo-create-scratch (&optional name directory owner description visibility make-readme gitignore-template license-key)
  "Create a new repository on github from scratch.

Description of Arguments:

 NAME               name of repository
 DIRECTORY          path to local directory of git repository
 OWNER              user/organization owning the repo
 DESCRIPTION        description for the repo
 VISIBILITY         private|public|internal
 MAKE-README        boolean, whether to make a readme file or not
 GITIGNORE-TEMPLATE name of gitignore template
 LICENSE-KEY        key for license template"
  (let* ((name (or name (read-string "Repository name: ")))
         (owner (or owner (consult--read (consult-gh--get-current-user-orgs nil t)
                                         :prompt "Repository owner: "
                                         :initial nil
                                         :sort nil
                                         :require-match t)))
         (targetrepo (concat (and owner (unless (string-empty-p owner) (concat owner "/"))) name))
         (description (or description (read-string "Description: ")))
         (description (and (stringp description)
                           (not (string-empty-p description))
                           description))
         (visibility (or visibility (downcase (consult--read (list "Public" "Private" "Internal")
                                                             :prompt "Visibility: "
                                                             :sort nil
                                                             :require-match t))))
         (readme (or make-readme (y-or-n-p "Would you like to add a README file?")))
         (gitignore (if (not gitignore-template) (y-or-n-p "Would you like to add a .gitignore?") t))
         (gitignore-template (or gitignore-template (and gitignore (string-trim (consult-gh--read-gitignore-template)))))
         (license (if (not license-key) (y-or-n-p "Would you like to add a license?") t))
         (license-key (or license-key (and license (consult-gh--read-license-key))))
         (confirm (y-or-n-p (format "This will create %s as a %s repository on GitHub.  Continue?" (propertize name 'face 'consult-gh-repo) (propertize visibility 'face 'warning))))
         (clone (if confirm (y-or-n-p "Clone the new repository locally?")))
         (clonedir (if clone (read-directory-name (format "Select Directory to clone %s in " (propertize name 'face 'font-lock-keyword-face)) (or directory (and (stringp consult-gh-default-clone-directory) (file-name-as-directory consult-gh-default-clone-directory)) default-directory))))
         (default-directory (or clonedir default-directory))
         (targetdir (expand-file-name name default-directory))
         (args '("repo" "create"))
         (out))

    (setq args (if (and targetrepo confirm visibility)
                   (delq nil (append args
                                     (list targetrepo)
                                     (list (concat "--" visibility))
                                     (and description (list "--description" description))
                                     (and readme (list "--add-readme"))
                                     (and gitignore-template (list "--gitignore" gitignore-template))
                                     (and license (list "--license" license-key))
                                     (and clone (list "--clone"))))))

    (setq out (apply #'consult-gh--call-process args))
    (if (eq (car out) 0)
        (when (and clone (file-exists-p targetdir))
          (message "repo %s was cloned to %s" (propertize name 'face 'font-lock-keyword-face) (propertize targetdir 'face 'font-lock-type-face))
          (run-hook-with-args 'consult-gh-repo-post-clone-hook targetdir)
          (propertize targetrepo :type 'new :name name :owner owner :directory (and clone targetdir) :license (and license license-key) :gitignore gitignore-template :make-readme readme :make-remote nil :visibiliy visibility :description description :template-repo nil))
      (progn (message (cadr out))))))

(defun consult-gh--repo-create-template (&optional name owner description visibility template)
  "Create a new repository on github from TEMPLATE repo.

Description of Arguments:

 NAME        name of repository
 OWNER       user/organization owning the repo
 DESCRIPTION description for the repo
 VISIBILITY  private|public|internal
 TEMPLATE    Full name of template repo \(e.g. user/repo\)"
  (let* ((name (or name (read-string "Repository name: ")))
         (owner (or owner (consult--read (consult-gh--get-current-user-orgs nil t)
                                         :prompt "Repository owner: "
                                         :initial nil
                                         :sort nil
                                         :require-match t)))
         (targetrepo (concat (and owner (unless (string-empty-p owner) (concat owner "/"))) name))
         (description (or description (read-string "Description: ")))
         (description (and (stringp description)
                           (not (string-empty-p description))
                           description))
         (visibility (or visibility (downcase (consult--read (list "Public" "Private" "Internal")
                                                             :prompt "Visibility: "
                                                             :sort nil
                                                             :require-match t))))
         (templates (consult-gh--get-user-template-repos))
         (template (or template (and templates (consult--read templates
                                                              :prompt "Select template repository: "
                                                              :sort nil
                                                              :require-match t))))
         (template (and (stringp template) (not (string-empty-p template)) template))
         (use-scratch (if (not template) (y-or-n-p "No template selected.  Would you like to make the repo without template?"))))
    (cond
     (template
      (let* ((confirm (y-or-n-p (format "This will create %s as a %s repository on GitHub.  Continue?" (propertize name 'face 'consult-gh-repo) (propertize visibility 'face 'warning))))
             (clone (if confirm (y-or-n-p "Clone the new repository locally?")))
             (clonedir (if clone (read-directory-name (format "Select Directory to clone %s in " (propertize name 'face 'font-lock-keyword-face)) (or (and (stringp consult-gh-default-clone-directory) (file-name-as-directory consult-gh-default-clone-directory)) default-directory))))
             (default-directory (or clonedir default-directory))
             (targetdir (expand-file-name name default-directory))
             (args '("repo" "create"))
             (out))

        (setq args (if (and targetrepo confirm visibility)
                       (delq nil (append args
                                         (list targetrepo)
                                         (list (concat "--" visibility))
                                         (list "--template" template)
                                         (and description (list "--description" description))
                                         (and clone (list "--clone"))))))

        (setq out (apply #'consult-gh--call-process args))
        (if (eq (car out) 0)
            (progn
              (when (and clone (file-exists-p targetdir))
              (message "repo %s was cloned to %s" (propertize name 'face 'font-lock-keyword-face) (propertize targetdir 'face 'font-lock-type-face))
              (run-hook-with-args 'consult-gh-repo-post-clone-hook targetdir))
              (propertize targetrepo :type 'template :name name :owner owner :directory (and clone targetdir) :license nil :gitignore nil :make-readme nil :make-remote nil :visibiliy visibility :description description :template-repo template))
          (message (cadr out)))))
     (use-scratch
      (consult-gh--repo-create-scratch name owner description visibility))
     (t
      (message "aborted without making repository")))))

(defun consult-gh--repo-create-push-existing (&optional name directory owner description visibility)
  "Create a new repository on github from local repo in DIRECTORY.

Description of arguments:
 DIRECTORY   path to local directory of git repository
 NAME        name of repository
 OWNER       user/organization owning the repo
 DESCRIPTION description for the repo
 VISIBILITY  private|public|internal"
  (let* ((directory (or directory (read-directory-name "Path to local repository: " default-directory)))
         (directory (and (stringp directory) (file-directory-p directory) (file-expand-wildcards (expand-file-name ".git" directory)) directory))
         (use-scratch (if (not directory) (y-or-n-p "No git directory selected.  Would you like to make the repo from scratch instead?"))))
    (cond
     (directory
      (let* ((name (or name (read-string "Repository name: " (file-name-nondirectory (string-trim-right directory "/")))))
             (owner (or owner (consult--read (consult-gh--get-current-user-orgs nil t)
                                             :prompt "Repository owner: "
                                             :initial nil
                                             :sort nil
                                             :require-match t)))
             (targetrepo (concat (and owner (unless (string-empty-p owner) (concat owner "/"))) name))
             (description (or description (read-string "Description: ")))
             (description (and (stringp description)
                               (not (string-empty-p description))
                               description))
             (visibility (or visibility (downcase (consult--read (list "Public" "Private" "Internal")
                                                                 :prompt "Visibility: "
                                                                 :sort nil
                                                                 :require-match t))))
             (confirm (y-or-n-p (format "This will create %s as a %s repository on GitHub.  Continue?" (propertize name 'face 'consult-gh-repo) (propertize visibility 'face 'warning))))
             (remote (and confirm (y-or-n-p "Add a remote?") (read-string "What should the new remote be called? " "origin")))
             (remote (and (stringp remote) (not (string-empty-p remote)) remote))
             (args '("repo" "create"))
             (out))

        (setq args (if (and targetrepo confirm visibility)
                       (delq nil (append args
                                         (list targetrepo)
                                         (list (concat "--" visibility))
                                         (list "--source" (file-truename directory))
                                         (and description (list "--description" description))
                                         (and remote (list "--remote" remote))))))
        (setq out (apply #'consult-gh--call-process args))
        (if (eq (car out) 0)
            (progn
              (message (cadr out))
              (propertize targetrepo :type 'push :name name :owner owner :directory (file-truename directory) :license nil :gitignore nil :make-readme nil :make-remote remote :visibiliy visibility :description description :template-repo nil))
          (message (cadr out)))))
     (use-scratch
      (consult-gh--repo-create-scratch name owner description visibility))
     (t
      (message "Aborted without making repository!")))))

(defvar consult-gh-repo-post-create-hook nil
  "Functions called after `consult-gh--repo-create'.

Full repo name with text properties is passed to these functions.

The text properties are:
  :type          a symbol, either \='new, \='template, or \='push
                 for new repos, repos pushed from a local folder,
                 or repos from a template
  :name          a string, name of the repo
  :owner         a string, name of the github user
  :directory     a string, the path where repo is cloned, if cloned
  :license       a string, the name of the lincense
  :gitignore     a string, the type of gitignore template used
  :make-readme   a boolean, whether a readme was made
  :make-remote   a boolean, whether a remote was made
  :visibiliy     a string, visibility of repo: private, public or internal
  :description   a string, description of the repo
  :template-repo a string, the name of the repo tempate,
                 if template was used.")

(defun consult-gh--repo-create (&optional name local-path owner description visibility make-readme gitignore-template license-key template)
  "Create a new repo with NAME and metadata on GitHub.

This mimicks the same interactive repo creation
from “gh repo create” in the command line.

Description of Arguments:

 NAME               a string; name of repository
 LOCAL-PATH         a string; path to local directory of git repository
 OWNER              a string; user/organization owning the repo
 DESCRIPTION        a string; description for the repo
 VISIBILITY         a string; private|public|internal
 MAKE-README        a boolean; whether to make a readme file or not
 GITIGNORE-TEMPLATE a string; name of gitignore template
 LICENSE-KEY        a string; key for license template
 TEMPLATE           a string; Full name of template repo \(e.g. user/repo\)

For more details, refer to the manual of “gh repo create” and see the
backend functions, `consult-gh--repo-create-scratch',
`consult-gh--repo-create-template', or
`consult-gh--repo-create-push-existing'."

  (let* ((answer (consult--read (list (cons "Create a new repository on GitHub from scratch" :scratch)
                                      (cons "Create a new repository on GitHub from a template repository" :template)
                                      (cons "Push an existing local repository to GitHub" :existing))
                                :prompt "What would you like to do?"
                                :lookup #'consult--lookup-cdr
                                :sort nil))
         (repo  (pcase answer
                  (':scratch (consult-gh--repo-create-scratch name local-path owner description visibility make-readme gitignore-template license-key))
                  (':template (consult-gh--repo-create-template name owner description visibility template))
                  (':existing (consult-gh--repo-create-push-existing name local-path owner description visibility)))))
    (when (and repo (stringp repo) (get-text-property 0 :type repo))
      (run-hook-with-args 'consult-gh-repo-post-create-hook repo))))

(defun consult-gh-topics--repo-parse-metadata ()
  "Parse repo topic metadata."
  (let* ((region (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (header (when region (buffer-substring-no-properties (car region) (cdr region))))
         (default-branch (when (and header (string-match ".*\\(?:\n.*default_branch:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (description (when (and header (string-match ".*\\(?:\n.*description:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (visibility (when (and header (string-match ".*\\(?:\n.*visibility:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (homepage-url (when (and header (string-match ".*\\(?:\n.*homepage:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (topics (when (and header (string-match ".*\\(?:\n.*topics:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (template (when (and header (string-match ".*\\(?:\n.*template:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (issues (when (and header (string-match ".*\\(?:\n.*issues:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)?" header))
                      (match-string 1 header)))

         (projects (when (and header (string-match ".*\\(?:\n.*projects:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))

         (discussions (when (and header (string-match ".*\\(?:\n.*discussions:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))

         (wiki (when (and header (string-match ".*\\(?:\n.*wiki:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))

         (merge-commit (when (and header (string-match ".*\\(?:\n.*merge_commit:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))

         (squash-merge (when (and header (string-match ".*\\(?:\n.*squash_merge:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (rebase-merge (when (and header (string-match ".*\\(?:\n.*rebase_merge:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header)))
         (delete-on-merge (when (and header (string-match ".*\\(?:\n.*delete_on_merge:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\|-\\)?" header))
                      (match-string 1 header))))

  (list (and (stringp default-branch)
             (string-trim default-branch))
        (and (stringp description)
             (string-trim description))
        (and (stringp visibility)
             (string-trim visibility))
        (and (stringp homepage-url)
             (string-trim homepage-url))
        (and (stringp topics)
             (string-replace "\n" ", " (string-trim topics)))
        (and (stringp template)
              (string-trim template))
        (and (stringp issues)
              (string-trim issues))
        (and (stringp projects)
              (string-trim projects))
        (and (stringp discussions)
             (string-trim discussions))
        (and (stringp wiki)
             (string-trim wiki))
        (and (stringp merge-commit)
              (string-trim merge-commit))
        (and (stringp squash-merge)
              (string-trim squash-merge))
        (and (stringp rebase-merge)
              (string-trim rebase-merge))
        (and (stringp delete-on-merge)
              (string-trim delete-on-merge)))))

(defun consult-gh-topics--repo-get-metadata (&optional repo)
  "Get metadata of REPO.

TOPIC defaults to `consult-gh--topic'."

  (let* ((repo (or repo consult-gh--topic))
         (default-branch (get-text-property 0 :default-branch repo))
         (description (get-text-property 0 :description repo))
         (visibility (get-text-property 0 :visibility repo))
         (homepage-url (get-text-property 0 :homepage-url repo))
         (repo-topics (get-text-property 0 :repo-repos repo))
         (template (get-text-property 0 :isTemplate repo))
         (issues (get-text-property 0 :issuesEnabled repo))
         (projects (get-text-property 0 :projectsEnabled repo))
         (discussions (get-text-property 0 :discussionsEnabled repo))
         (wiki (get-text-property 0 :wikiEnabled repo))
         (merge-commit (get-text-property 0 :mergeCommitAllowed repo))
         (squash-merge (get-text-property 0 :squashMergeAllowed repo))
         (rebase-merge (get-text-property 0 :rebaseMergeAllowed repo))
         (delete-on-merge (get-text-property 0 :deleteOnMerge repo)))

      (pcase-let* ((`(,text-default-branch ,text-description ,text-visibility ,text-homepage-url ,text-repo-topics ,text-template ,text-issues ,text-projects ,text-discussions ,text-wiki ,text-merge-commit ,text-squash-merge ,text-rebase-merge ,text-delete-on-merge) (consult-gh-topics--repo-parse-metadata)))

        (when (stringp text-default-branch)
          (setq default-branch (string-trim text-default-branch)))

        (when (stringp text-description)
          (setq description text-description))

        (when (and (stringp text-visibility)
                   (member (downcase text-visibility) '("private" "public" "internal")))
          (setq visibility text-visibility))

        (when (stringp text-homepage-url)
          (setq homepage-url text-homepage-url))

        (when (stringp text-repo-topics)
          (setq repo-topics (cl-remove-duplicates
                         (split-string text-repo-topics "," t "[ \t]+")
                        :test #'equal)))

        (when (stringp text-template)
          (setq template (cond
                        ((equal (downcase text-template) "true") t)
                        ((equal (downcase text-template) "false") nil)
                        (t template))))

        (when (stringp text-issues)
          (setq issues (cond
                        ((equal (downcase text-issues) "enabled") t)
                        ((equal (downcase text-issues) "disabled") nil)
                        (t issues))))

        (when (stringp text-projects)
          (setq projects (cond
                        ((equal (downcase text-projects) "enabled") t)
                        ((equal (downcase text-projects) "disabled") nil)
                        (t projects))))

        (when (stringp text-discussions)
          (setq discussions (cond
                        ((equal (downcase text-discussions) "enabled") t)
                        ((equal (downcase text-discussions) "disabled") nil)
                        (t discussions))))

        (when (stringp text-wiki)
          (setq wiki (cond
                        ((equal (downcase text-wiki) "enabled") t)
                        ((equal (downcase text-wiki) "disabled") nil)
                        (t wiki))))

        (when (stringp text-merge-commit)
          (setq merge-commit (cond
                        ((equal (downcase text-merge-commit) "allowed") t)
                        ((equal (downcase text-merge-commit) "not allowed") nil)
                        (t merge-commit))))

        (when (stringp text-squash-merge)
         (setq squash-merge (cond
                        ((equal (downcase text-squash-merge) "allowed") t)
                        ((equal (downcase text-squash-merge) "not allowed") nil)
                        (t squash-merge))))

        (when (stringp text-rebase-merge)
          (setq rebase-merge (cond
                        ((equal (downcase text-rebase-merge) "allowed") t)
                        ((equal (downcase text-rebase-merge) "not allowed") nil)
                        (t rebase-merge))))

        (when (stringp text-delete-on-merge)
          (setq delete-on-merge (cond
                        ((equal (downcase text-delete-on-merge) "yes") t)
                        ((equal (downcase text-delete-on-merge) "no") nil)
                        (t delete-on-merge))))

    (list (cons "default-branch" default-branch)
          (cons "description" description)
          (cons "visibility" visibility)
          (cons "homepage-url" homepage-url)
          (cons "topics" repo-topics)
          (cons "template" template)
          (cons "issues" issues)
          (cons "projects" projects)
          (cons "discussions" discussions)
          (cons "wiki" wiki)
          (cons "merge-commit" merge-commit)
          (cons "squash-merge" squash-merge)
          (cons "rebase-merge" rebase-merge)
          (cons "delete-on-merge" delete-on-merge)))))

(defun consult-gh-repo--edit-restore-default (&optional repo)
  "Restore default values when editing a REPO."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (repo-name (substring-no-properties (get-text-property 0 :repo repo)))
             (canadmin (consult-gh--user-canadmin repo-name))
             (default-branch (get-text-property 0 :original-default-branch repo))
             (description (get-text-property 0 :original-description repo))
             (visibility (get-text-property 0 :original-visibility repo))
             (homepage-url (get-text-property 0 :original-homepage-url repo))
             (repo-topics (get-text-property 0 :original-repo-topics repo))
             (template (get-text-property 0 :original-isTemplate repo))
             (issues (get-text-property 0 :original-issuesEnabled repo))
             (projects (get-text-property 0 :original-projectsEnabled repo))
             (discussions (get-text-property 0 :original-discussionsEnabled repo))
             (wiki (get-text-property 0 :original-wikiEnabled repo))
             (merge-commit (get-text-property 0 :original-mergeCommitAllowed repo))
             (squash-merge (get-text-property 0 :original-squashMergeAllowed repo))
             (rebase-merge (get-text-property 0 :original-rebaseMergeAllowed repo))
             (delete-on-merge (get-text-property 0 :original-deleteOnMerge repo))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))

        (when canadmin
          (add-text-properties 0 1 (list :default-branch default-branch :description description :visibility visibility :homepage-url homepage-url :repo-topics repo-topics :isTemplate template :issuesEnabled issues :projectsEnabled projects :discussionsEnabled discussions :wikiEnabled wiki :mergeCommitAllowed merge-commit :squashMergeAllowed squash-merge :rebaseMergeAllowed rebase-merge :deleteOnMerge delete-on-merge) repo)

          (save-excursion
            ;; restore default branch
            (goto-char (point-min))
            (when (re-search-forward "^.*default_branch: \\(?1:.*\\)?" nil t)
              (replace-match (get-text-property 0 :default-branch repo) nil nil nil 1))

            ;; restore description
            (goto-char (point-min))
            (when (re-search-forward "^.*description: \\(?1:.*\\)?" nil t)
              (replace-match (get-text-property 0 :description repo) nil nil nil 1))

            ;; restore visibility
            (goto-char (point-min))
            (when (re-search-forward "^.*visibility: \\(?1:.*\\)?" nil t)
              (replace-match (get-text-property 0 :visibility repo) nil nil nil 1))

            ;; restore homepage
            (goto-char (point-min))
            (when (re-search-forward "^.*homepage: \\(?1:.*\\)?" nil t)
              (replace-match (get-text-property 0 :homepage-url repo) nil nil nil 1))

            ;; restore topics
            (goto-char (car header))
            (when (re-search-forward "^.*topics: \\(?1:.*\\)?" (cdr header) t)
              (replace-match (mapconcat #'identity (get-text-property 0 :repo-topics repo) ", ") nil nil nil 1))

            ;; restore template
            (goto-char (point-min))
            (when (re-search-forward "^.*template: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :isTemplate repo) "true" "false") nil nil nil 1))

            ;; restore issues
            (goto-char (point-min))
            (when (re-search-forward "^.*issues: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :issuesEnabled repo) "enabled" "disabled") nil nil nil 1))

            ;; restore projects
            (goto-char (point-min))
            (when (re-search-forward "^.*projects: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :projectsEnabled repo) "enabled" "disabled") nil nil nil 1))

            ;; restore discussions
            (goto-char (point-min))
            (when (re-search-forward "^.*discussions: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :discussionsEnabled repo) "enabled" "disabled") nil nil nil 1))

            ;; restore wiki
            (goto-char (point-min))
            (when (re-search-forward "^.*wiki: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :wikiEnabled repo) "enabled" "disabled") nil nil nil 1))

            ;; restore merge commit
            (goto-char (point-min))
            (when (re-search-forward "^.*merge_commit: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :mergeCommitAllowed repo) "allowed" "not allowed") nil nil nil 1))

            ;; restore squash merge
            (goto-char (point-min))
            (when (re-search-forward "^.*squash_merge: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :squashMergeAllowed repo) "allowed" "not allowed") nil nil nil 1))

            ;; restore rebase merge
            (goto-char (point-min))
            (when (re-search-forward "^.*rebase_merge: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :rebaseMergeAllowed repo) "allowed" "not allowed") nil nil nil 1))

            ;; restore delete on merge
            (goto-char (point-min))
            (when (re-search-forward "^.*delete_on_merge: \\(?1:.*\\)?" nil t)
              (replace-match (if (get-text-property 0 :deleteOnMerge repo) "yes" "no") nil nil nil 1)))))
    (error "Not in an issue editing buffer!")))

(defun consult-gh-repo--edit-readme (&optional repo)
  "Edit readme of REPO."
  (cond
   ((and consult-gh-topics-edit-mode
         (equal (get-text-property 0 :type consult-gh--topic) "repo"))
    (with-current-buffer (get-text-property 0 :view-buffer consult-gh--topic) (consult-gh-repo--edit-readme)))
   (consult-gh-repo-view-mode
    (let* ((repo (or repo consult-gh--topic))
           (repo-name (substring-no-properties (get-text-property 0 :repo repo)))
           (path (get-text-property 0 :readme-path repo))
           (ref (get-text-property 0 :default-branch repo))
           (size (get-text-property 0 :readme-size repo))
           (api-url (get-text-property 0 :readme-api-url repo))
           (newtopic (format "%s/%s" repo-name path)))
    (add-text-properties 0 1 (list :repo repo-name :type "file" :path path :api-url api-url :size size :ref ref) newtopic)
    (with-current-buffer (funcall #'consult-gh--files-view-action newtopic)
    (consult-gh-edit-file))))
   (t (let* ((repo (or repo consult-gh--topic))
             (repo-name (substring-no-properties (get-text-property 0 :repo repo)))
             (cand (propertize repo-name :repo repo-name)))
        (when repo-name
          (with-current-buffer (funcall #'consult-gh--repo-view-action cand)
            (consult-gh-repo--edit-readme)))))))

(defun consult-gh-repo--edit-default-branch (&optional new repo)
  "Change default branch of REPO to NEW branch."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (repo-name (get-text-property 0 :repo repo))
             (new (or new (consult-gh--read-branch repo-name nil "New Default Branch Name: " nil t)))
             (new (and (stringp new)
                       (not (string-empty-p new))
                       new))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (when (stringp new)
       (add-text-properties 0 1 (list :default-branch (substring-no-properties new)) repo)
      (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*default_branch: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (get-text-property 0 :default-branch repo) nil nil nil 1)))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-description (&optional new old repo)
  "Change description of REPO from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (or new (consult--read nil
                                     :initial old
                                     :prompt "New Description: ")))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :description new) repo)

    (when (stringp new)
      (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*description: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (get-text-property 0 :description repo) nil nil nil 1)))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-visibility (&optional new old repo)
  "Change visibility of REPO from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (or new (consult--read (list "PRIVATE" "PUBLIC" "INTERNAL")
                                     :initial old
                                     :prompt "Visibility: ")))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :visibility new) repo)

    (when (stringp new)
      (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*visibility: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (get-text-property 0 :visibility repo) nil nil nil 1)))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-homepage-url (&optional new old repo)
  "Change homepage of REPO from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (or new (consult--read nil
                                     :initial old
                                     :prompt "New URL: ")))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :homepage-url new) repo)

    (when (stringp new)
      (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*homepage: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (get-text-property 0 :homepage-url repo) nil nil nil 1)))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-change-topics (&optional new old repo)
  "Change topics of REPO from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (old (cond ((stringp old) old)
                        ((and (listp old) (length> old 1)) (mapconcat #'identity old sep))
                        ((and (listp old) (length< old 2)) (car old))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (pop-topics (or (get-text-property 0 :popular-topics repo) (consult-gh--json-to-hashtable (consult-gh--api-get-command-string "/repos/github/explore/contents/topics/") :name)))
             (new (or new
                      (completing-read-multiple "Enter/Select Topics: " pop-topics nil t old))))

        (when (listp new)
          (setq new (cl-remove-duplicates new :test #'equal))
          (add-text-properties 0 1 (list :topics new) repo)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*topics: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :topics repo) ", ") nil nil nil 1)))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-template (&optional old repo)
  "Toggle REPO to (non)template.

OLD is a boolean, whether repo is currently a template."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :isTemplate new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*template: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :isTemplate repo) "true" "false")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-issues (&optional old repo)
  "Toggle issues in REPO.

OLD is a boolean, whether issues are currently enabled."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :issuesEnabled new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*issues: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :issuesEnabled repo) "enabled" "disabled")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-projects (&optional old repo)
  "Toggle projects in REPO.

OLD is a boolean, whether projects are currently enabled."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :projectsEnabled new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*projects: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :projectsEnabled repo) "enabled" "disabled")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-discussions (&optional old repo)
  "Toggle discussions in REPO.

OLD is a boolean, whether discussions are currently enabled."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :discussionsEnabled new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*discussions: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :discussionsEnabled repo) "enabled" "disabled")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-wiki (&optional old repo)
  "Toggle wiki in REPO.

OLD is a boolean, whether wiki is currently enabled."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :wikiEnabled new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*wiki: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :wikiEnabled repo) "enabled" "disabled")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-merge-commit (&optional old repo)
  "Toggle merge commit in REPO.

OLD is a boolean, whether merge commit is currently allowed."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :mergeCommitAllowed new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*merge_commit: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :mergeCommitAllowed repo) "allowed" "not allowed")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-squash-merge (&optional old repo)
  "Toggle squash merge in REPO.

OLD is a boolean, whether squash merge is currently allowed."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :squashMergeAllowed new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*squash_merge: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :squashMergeAllowed repo) "allowed" "not allowed")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-rebase-merge (&optional old repo)
  "Toggle rebase merge in REPO.

OLD is a boolean, whether rebase merge is currently allowed."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (repo (get-text-property 0 :repo repo))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :rebaseMergeAllowed new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*rebase_merge: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :rebaseMergeAllowed repo) "allowed" "not allowed")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun consult-gh-repo--edit-toggle-delete-on-merge (&optional old repo)
  "Toggle delete on merge in REPO.

OLD is a boolean, whether delete on merge is currently enabled."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (new (not old))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :deleteOnMerge new) repo)

    (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*delete_on_merge: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (if (get-text-property 0 :deleteOnMerge repo) "yes" "no")
                                       nil nil nil 1))))
    (error "Not in a repo editing buffer!")))

(defun  consult-gh-topics--edit-repo-submit (repo &optional default-branch description visibility homepage-url repo-topics template issues discussions wiki projects merge-commit squash-merge rebase-merge  delete-on-merge)
  "Edit REPO with new metadata.

Description of Arguments:
  REPO                  a propertized string; properties are repo
                        details.  Defaults to `consult-gh--topic'.
  DEFAULT-BRANCH        a string; name of the default branch
  DESCRIPTION           a string; description of repo
  VISIBILITY            a string; visibility of repo
                        “private”, “public” or“internal”
  HOMEPAGE-URL          a string; homepage url of repo
  REPO-TOPICS           a list; list of relevant topics of repo
  TEMPLATE              a boolean; whether repo is a template
  ISSUES                a boolean; whether issues are enabled
  DISCUSSIONS           a boolean; whether discussions are enabled
  WIKI                  a boolean; whether wiki is enabled
  PROJECTS              a boolean; whether projects are enabled
  MERGE-COMMIT          a boolean; whether merge commit is allowed
  SQUASH-MERGE          a boolean; whether squash merge is allowed
  REBASE-MERGE          a boolean; whether rebase merge is allowed
  DELETE-ON-MERGE       a boolean; whether to delete head branch on merge"

  (pcase-let* ((repo (or repo consult-gh--topic))
               (repo-name (or (get-text-property 0 :repo repo)
                              (get-text-property 0 :repo (consult-gh-user-repos nil t))))
               (canadmin (consult-gh--user-canadmin repo-name))
               (original-default-branch (get-text-property 0 :original-default-branch repo))
               (original-description (get-text-property 0 :original-description repo))
               (original-visibility (get-text-property 0 :original-visibility repo))
               (original-homepage-url (get-text-property 0 :original-homepage-url repo))
               (original-repo-topics (get-text-property 0 :original-repo-topics repo))
               (original-template (get-text-property 0 :original-isTemplate repo))
               (original-issues (get-text-property 0 :original-issuesEnabled repo))
               (original-projects (get-text-property 0 :original-projectsEnabled repo))
               (original-discussions (get-text-property 0 :original-discussionsEnabled repo))
               (original-wiki (get-text-property 0 :original-wikiEnabled repo))
               (original-merge-commit (get-text-property 0 :original-mergeCommitAllowed repo))
               (original-squash-merge (get-text-property 0 :original-squashMergeAllowed repo))
               (original-rebase-merge (get-text-property 0 :original-rebaseMergeAllowed repo))
               (original-delete-on-merge (get-text-property 0 :original-deleteOnMerge repo))
               (change-default-branch (and (not (equal default-branch original-default-branch)) (stringp default-branch) (not (string-empty-p default-branch))  default-branch))
               (change-description (and (not (equal description original-description)) (stringp description) description))
               (change-homepage (and (not (equal homepage-url original-homepage-url)) (stringp homepage-url) homepage-url))
               (`(,add-topics ,remove-topics)                          (consult-gh--separate-add-and-remove repo-topics original-repo-topics))
               (change-template (when (not (equal template original-template)) (if template "true" "false")))
               (change-visibility  (when (and (stringp visibility)
                                              (not (equal visibility original-visibility))
                                              (member (downcase visibility) '("private" "public" "internal"))
                                              (y-or-n-p (format "%s You are about to change the visibility of repo, %s to %s.  Do you accept the consequences? " (propertize "DANGER ZONE!" 'face 'error) (propertize repo 'face 'consult-gh-repo) (propertize (downcase visibility) 'face 'warning))))
                                     (downcase visibility)))
               (enable-issues (when (not (equal issues original-issues))
                                (if issues
                                    "true"
                                  "false")))
               (enable-projects (when (not (equal projects original-projects))
                                  (if projects
                                      "true"
                                    "false")))
               (enable-discussions (when (not (equal discussions original-discussions))
                                     (if discussions
                                         "true"
                                       "false")))
               (enable-wiki (when (not (equal wiki original-wiki))
                              (if wiki
                                  "true"
                                "false")))
               (enable-merge-commit (when (not (equal merge-commit original-merge-commit))
                                      (if merge-commit
                                          "true"
                                        "false")))
               (enable-squash-merge (when (not (equal squash-merge original-squash-merge))
                                      (if squash-merge
                                          "true"
                                        "false")))
               (enable-rebase-merge (when (not (equal rebase-merge original-rebase-merge))
                                      (if rebase-merge
                                          "true"
                                        "false")))
               (delete-branch-on-merge (when (not (equal delete-on-merge original-delete-on-merge))
                                         (if delete-on-merge
                                             "true"
                                           "false")))
               (args (list)))

    (if canadmin
        (cond
         ((or change-default-branch change-description change-visibility change-homepage add-topics remove-topics change-template enable-issues enable-projects enable-discussions enable-wiki enable-merge-commit enable-squash-merge enable-rebase-merge delete-branch-on-merge)

          (when (and add-topics (listp add-topics)) (setq add-topics (consult-gh--list-to-string add-topics)))
          (when (and remove-topics (listp remove-topics)) (setq remove-topics (consult-gh--list-to-string remove-topics)))

          (setq args (delq nil (append args
                                       (and change-default-branch (list "--default-branch" (concat (substring-no-properties change-default-branch))))
                                       (and change-description (list "--description" (concat (substring-no-properties change-description))))
                                       (and change-visibility (list "--visibility" (substring-no-properties change-visibility) "--accept-visibility-change-consequences"))
                                       (and change-homepage (list "--homepage" (concat (substring-no-properties change-homepage))))
                                       (and add-topics (list "--add-topic" add-topics))
                                       (and remove-topics (list "--remove-topic" remove-topics))
                                       (and change-template (list (format "--template=%s" change-template)))
                                       (and enable-issues (list (format "--enable-issues=%s" enable-issues)))
                                       (and enable-projects (list (format "--enable-projects=%s" enable-projects)))
                                       (and enable-discussions (list (format "--enable-discussions=%s" enable-discussions)))
                                       (and enable-wiki (list (format "--enable-wiki=%s" enable-wiki)))
                                       (and enable-merge-commit (list (format "--enable-merge-commit=%s" enable-merge-commit)))
                                       (and enable-squash-merge (list (format "--enable-squash-merge=%s" enable-squash-merge)))
                                       (and enable-rebase-merge (list (format "--enable-rebase-merge=%s" enable-rebase-merge)))
                                       (and delete-branch-on-merge (list (format "--delete-branch-on-merge=%s" delete-branch-on-merge))))))

          (apply #'consult-gh--command-to-string "repo" "edit" (substring-no-properties repo-name) args))
         (t (message "Nothing to change!")
            nil))
      (progn (message "User does not have admin rights to change the repo.")
             nil))))

(defun consult-gh-topics--edit-repo-presubmit (repo)
  "Prepare edits on REPO to submit.

REPO is a string with properties that identify a github repo.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--repo-view'."
  (if consult-gh-topics-edit-mode
      (let* ((repo (or repo consult-gh--topic))
             (repo-name (get-text-property 0 :repo repo))
             (canadmin (consult-gh--user-canadmin repo-name))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (is-template (get-text-property 0 :isTemplate repo))
             (nextsteps (if canadmin
                            (append (list (cons "Submit" :submit))
                                    (list (cons "Edit Readme" :readme))
                                    (list (cons "Change Default Branch" :default-branch))
                                    (list (cons "Change Visibility" :visibility))
                                    (list (cons "Edit Description" :description))
                                    (list (cons "Edit Homepage URL" :homepage-url))
                                    (list (cons "Edit Topics" :topics))
                                    (if is-template (list (cons "Make Repository a non Template" :template))
                                      (list (cons "Make Repository a Template" :template)))
                                    (list (cons "Enable/Disable Issues" :issues))
                                    (list (cons "Enable/Disable Projects" :projects))
                                    (list (cons "Enable/Disable Discussions" :discussions))
                                    (list (cons "Enable/Disable Wiki" :wiki))
                                    (list (cons "(Dis)Allow Merge Commit" :merge-commit))
                                    (list (cons "(Dis)Allow Squash Merge" :squash-merge))
                                    (list (cons "(Dis)Allow Rebase Merge" :rebase-merge))
                                    (list (cons "Toggle Delete on Merge" :delete-on-merge))
                                    (list (cons "Discard edits and restore original values" :default))
                                    (list (cons "Cancel" :cancel)))
                          (user-error "Current user, %s, does not have permissions to edit this pull request" user)))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))

        (pcase-let* (
                     (metadata (consult-gh-topics--repo-get-metadata))
                     (default-branch  (when metadata (cdr (assoc "default-branch" metadata))))
                     (description  (when metadata (cdr (assoc "description" metadata))))
                     (visibility  (when metadata (cdr (assoc "visibility" metadata))))
                     (homepage-url  (when metadata (cdr (assoc "homepage-url" metadata))))
                     (repo-topics  (when metadata (cdr (assoc "topics" metadata))))
                     (template  (when metadata (cdr (assoc "template" metadata))))
                     (issues  (when metadata (cdr (assoc "issues" metadata))))
                     (projects  (when metadata (cdr (assoc "projects" metadata))))
                     (discussions  (when metadata (cdr (assoc "discussions" metadata))))
                     (wiki  (when metadata (cdr (assoc "wiki" metadata))))
                     (merge-commit  (when metadata (cdr (assoc "merge-commit" metadata))))
                     (squash-merge  (when metadata (cdr (assoc "squash-merge" metadata))))
                     (rebase-merge  (when metadata (cdr (assoc "rebase-merge" metadata))))
                     (delete-on-merge  (when metadata (cdr (assoc "delete-on-merge" metadata)))))


          (pcase next
            (':readme (consult-gh-repo--edit-readme))
            (':default-branch (consult-gh-repo--edit-default-branch))
            (':description (consult-gh-repo--edit-description nil description))
            (':visibility (consult-gh-repo--edit-visibility nil visibility))
            (':homepage-url (consult-gh-repo--edit-homepage-url nil homepage-url))
            (':topics (consult-gh-repo--edit-change-topics nil repo-topics))
            (':template (consult-gh-repo--edit-toggle-template template))
            (':issues (consult-gh-repo--edit-toggle-issues issues))
            (':projects (consult-gh-repo--edit-toggle-projects projects))
            (':discussions (consult-gh-repo--edit-toggle-discussions discussions))
            (':wiki (consult-gh-repo--edit-toggle-wiki wiki))
            (':merge-commit (consult-gh-repo--edit-toggle-merge-commit merge-commit))
            (':squash-merge (consult-gh-repo--edit-toggle-squash-merge squash-merge))
            (':rebase-merge (consult-gh-repo--edit-toggle-rebase-merge rebase-merge))
            (':delete-on-merge (consult-gh-repo--edit-toggle-delete-on-merge delete-on-merge))
            (':default (consult-gh-repo--edit-restore-default))
            (':submit
             (and (consult-gh-topics--edit-repo-submit nil default-branch description visibility homepage-url repo-topics template issues discussions wiki projects merge-commit squash-merge rebase-merge  delete-on-merge)
                  (message "Edits Submitted!")
                  (funcall consult-gh-quit-window-func t))))))
    (message "Not in a repo editing buffer!")))

(defun consult-gh--issue-list-format (string input highlight)
  "Format minibuffer candidates for issues.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh issue list ...”\).
  INPUT     the query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
            with `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "issue")
         (type "issue")
         (parts (string-split string "\t"))
         (repo (car (consult--command-split input)))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (number (car parts))
         (state (upcase (cadr parts)))
         (face (pcase state
                 ("CLOSED" 'consult-gh-success)
                 ("OPEN" 'consult-gh-warning)
                 (_ 'consult-gh-issue)))
         (title (cadr (cdr parts)))
         (tags (cadr (cdr (cdr parts))))
         (date (cadr (cdr (cdr (cdr parts)))))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width (concat (propertize (format "%s" number) 'face face) ":" (propertize (format "%s" title) 'face 'consult-gh-default)) 70)
                      (propertize (consult-gh--set-string-width state 8) 'face face)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize (consult-gh--set-string-width tags 18) 'face 'consult-gh-tags)
                      (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user)) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :package package :number number :state state :title title :tags tags :date date :query query :class class :type type) str)
    str))

(defun consult-gh--search-issues-format (string input highlight)
  "Format candidates for issues.

Description of Arguments:

  STRING the output of a “gh” call
         \(e.g. “gh search issues ...”\).
  INPUT  the query from the user
         \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
           with `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "issue")
         (type "issue")
         (parts (string-split string "\t"))
         (repo (car parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (number (cadr parts))
         (state (upcase (cadr (cdr parts))))
         (face (pcase state
                 ("CLOSED" 'consult-gh-success)
                 ("OPEN" 'consult-gh-warning)
                 (_ 'consult-gh-issue)))
         (title (cadr (cdr (cdr parts))))
         (tags (cadr (cdr (cdr (cdr parts)))))
         (date (cadr (cdr (cdr (cdr (cdr parts))))))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width (concat (propertize (format "%s" number) 'face face) ":" (propertize (format "%s" title) 'face 'consult-gh-default)) 70)
                      (propertize (consult-gh--set-string-width state 8) 'face face)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize (consult-gh--set-string-width tags 18) 'face 'consult-gh-tags)
                      (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user )) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo
                     :user user
                     :package package
                     :number number
                     :state state
                     :title title
                     :tags tags
                     :date date
                     :query query
                     :class class
                     :type type)
                         str)
    str))

(defun consult-gh--search-issues-include-prs-format (string input highlight)
  "Format candidates for issues.

Description of Arguments:

  STRING the output of a “gh” call
         \(e.g. “gh search issues ...”\).
  INPUT  the query from the user
         \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
           with `consult-gh-highlight-match' in the minibuffer."
  (let* ((parts (string-split string "\t"))
         (type (car parts))
         (class type)
         (repo (cadr parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (number (caddr parts))
         (state (upcase (caddr (cdr parts))))
         (face (pcase state
                 ("CLOSED" 'consult-gh-success)
                 ("OPEN" 'consult-gh-warning)
                 (_ 'consult-gh-issue)))
         (title (caddr (cdr (cdr parts))))
         (tags (caddr (cdr (cdr (cdr parts)))))
         (date (caddr (cdr (cdr (cdr (cdr parts))))))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width (concat (propertize (format "%s" number) 'face face) ":" (propertize (format "%s" title) 'face 'consult-gh-default)) 70)
                      (propertize (consult-gh--set-string-width type 5) 'face 'consult-gh-description)
                      (propertize (consult-gh--set-string-width state 8) 'face face)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize (consult-gh--set-string-width tags 18) 'face 'consult-gh-tags)
                      (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user )) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo
                                   :user user
                                   :package package
                                   :number number
                                   :state state
                                   :title title
                                   :tags tags
                                   :date date
                                   :query query
                                   :class class
                                   :type type)
                         str)
    str))

(defun consult-gh--issue-state ()
  "State function for issue candidates.

This is passed as STATE to `consult--read' in `consult-gh-search-issues'
and is used to preview or do other actions on the issue."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (query (get-text-property 0 :query cand))
                        (number (get-text-property 0 :number cand))
                        (match-str (consult--build-args query))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (consult-gh--issue-view (format "%s" repo) (format "%s" number) buffer t)
               (with-current-buffer buffer
                 (if consult-gh-highlight-matches
                     (cond
                      ((listp match-str)
                       (mapc (lambda (item)
                                 (highlight-regexp item 'consult-gh-preview-match)) match-str))
                      ((stringp match-str)
                       (highlight-regexp match-str 'consult-gh-preview-match)))))
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun consult-gh--issue-group (cand transform)
  "Group function for issue.

This is passed as GROUP to `consult--read' in `consult-gh-issue-list'
or `consult-gh-search-issues', and is used to group issues.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-issues-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Number:Title " 68 nil ?-)
       (consult-gh--set-string-width " State " 10 nil ?-)
       (consult-gh--set-string-width " Date " 12 nil ?-)
       (consult-gh--set-string-width " Tags " 20 nil ?-)
       (consult-gh--set-string-width " Repo " 40 nil ?-))))))

(defun consult-gh--issue-browse-url-action (cand)
  "Browse the url for an issue candidate, CAND.

This is an internal action function that gets an issue candidate, CAND,
from `consult-gh-search-issues' and opens the url of the issue
in an external browser.

To use this as the default action for issues,
set `consult-gh-issue-action' to `consult-gh--issue-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (number (substring-no-properties (get-text-property 0 :number cand)))
         (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")))
         (url (and repo-url (concat repo-url "/issues/" number))))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--issue-read-json (repo number &optional json)
  "Get json response of issue of NUMBER in REPO.

Runs an async shell command with the command:
gh issue view NUMBER --repo REPO --json JSON,
and returns the output as a hash-table.

Optional argument JSON defaults to `consult-gh--issue-view-json-fields'."
  (let* ((json-object-type 'hash-table)
         (json-array-type 'list)
         (json-key-type 'keyword)
         (json-false :false))
    (json-read-from-string (consult-gh--command-to-string "issue" "view" number "--repo" repo "--json" (or json consult-gh--issue-view-json-fields)))))

(defun consult-gh--issue-get-comments (repo number)
  "Get comments of issue NUMBER in REPO.

Retrieves a list of comments issue with id NUMBER in REPO.
Optional argument maxnum limits the number of comments retrieved."
  (consult-gh--json-to-hashtable (consult-gh--command-to-string "issue" "view" number "--repo" repo "--json" "comments") :comments))

(defun consult-gh--issue-get-commenters (table &optional comments)
  "Get a list of related users to an issue.

Retrieves a list of all related commenter users for the issue
stored in TABLE, a hash-table output
from `consult-gh--issue-read-json'.

Optional argument COMMENTS is a list o comments, for example
from running “gh issue view” with argument “--json comments”"
  (let* ((author (gethash :login (gethash :author table)))
         (assignees (gethash :assignees table))
         (assignees (and (listp assignees) (mapcar (lambda (item) (and (hash-table-p item) (gethash :login item))) assignees)))
         (comments (or comments (gethash :comments table)))
         (commenters (when (and comments (listp comments)) (cl-loop for comment in comments
                                                                    collect
                                                                    (when (hash-table-p comment)
                                                                      (gethash :login (gethash :author comment)))))))
         (cl-remove-duplicates (delq nil (append (list author) assignees commenters)) :test #'equal)))

(defun consult-gh--issue-format-header (repo number table &optional topic)
  "Format a header for an issue of NUMBER in REPO.

TABLE is a hash-table output containing issue information
from `consult-gh--issue-read-json'.  Returns a formatted string containing
the header section for `consult-gh--issue-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--issue-view'."
  (let* ((title (gethash :title table))
         (author (gethash :login (gethash :author table)))
         (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author) 'rear-nonsticky t)))
         (state (gethash :state table))
         (createdAt (gethash :createdAt table))
         (createdAt (and createdAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time createdAt))))

         (updatedAt (gethash :updatedAt table))
         (updatedAt (and updatedAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time updatedAt))))
         (html-url (gethash :url table))
         (closedAt (gethash :closedAt table))
         (closedAt (and closedAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time closedAt))))
         (labels (gethash :labels table))
         (labels (and labels (listp labels) (mapcar (lambda (item)
                                                              (when (hash-table-p item)
                                                                (let* ((name (gethash :name item))
                                                                       (desc (gethash :description item))
                                                                       (color (gethash :color item)))
                                                                  (when (stringp name)
                                                                    (propertize name 'face `(t :background ,(concat "#" color) :box (:color ,(concat "#" color) :line-width (-1 . -2))) 'help-echo (apply-partially #'consult-gh--get-label-tooltip name desc color) 'rear-nonsticky t)))))
                                                    labels)))
         (labels-text (and labels (listp labels) (mapconcat #'identity labels "\s\s")))
         (milestone (gethash :milestone table))
         (milestone-title (and (hash-table-p milestone) (gethash :title milestone)))
         (milestone-text (and (hash-table-p milestone) (propertize milestone-title 'help-echo (concat (format "%s\n%s"
                                                                                                              (or (gethash :title milestone) "")
                                                                                                              (or (gethash :description milestone) "no description")))
                                                                   'rear-nonsticky t)))
         (assignees (gethash :assignees table))
         (assignees (and assignees (listp assignees) (mapcar (lambda (item)
                                                                (when (hash-table-p item)
                                                                (let* ((login (gethash :login item)))
                                                                  (if (stringp login)
                                                                    (propertize login 'help-echo (apply-partially #'consult-gh--get-user-tooltip login) 'rear-nonsticky t)
                                                                    login))))
                                                             assignees)))
         (assignees-text (and assignees (listp assignees) (mapconcat #'identity assignees ",\s")))
         (projects (gethash :projectItems table))
         (projects (and projects (listp projects) (mapcar (lambda (item) (when (hash-table-p item) (gethash :title item))) projects)))
         (projects-text (and projects (listp projects) (mapconcat #'identity projects ",\s"))))

    (when (stringp topic)
      (add-text-properties 0 1 (list :author author :state state :created createdAt :closed closedAt :lastUpdated updatedAt :labels labels :milestone milestone-title :assignees assignees :projects projects) topic))

    (concat "title: " title "\n"
            "author: " author "\n"
            "repository: " (propertize repo 'help-echo (apply-partially #'consult-gh--get-repo-tooltip repo)) "\n"
            "number: " number "\n"
            "state: " state "\n"
            (and createdAt (concat "created: " createdAt "\n"))
            (and updatedAt (concat "lastUpdated: " updatedAt "\n"))
            (and closedAt (concat "closed: " closedAt "\n"))
            (and html-url (concat "url: " html-url "\n"))
            (and assignees-text (concat "assignees: " assignees-text "\n"))
            (and labels-text (concat "labels: " "[ " labels-text " ]""\n"))
            (and milestone-text (concat "milestone: " milestone-text "\n"))

            (and projects-text (concat "projects: " projects-text "\n"))
            "\n--\n")))

(defun consult-gh--issue-format-body (table &optional topic)
  "Format a body section for an issue stored in TABLE.

This function returns a formatted string containing the body section for
`consult-gh--issue-view'.

TABLE is a hash-table output from `consult-gh--issue-read-json'
containing issue's body under the key :body.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--issue-view'."
  (let* ((author (gethash :login (gethash :author table)))
         (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author))))
         (body (gethash :body table))
         (createdAt (gethash :createdAt table))
         (header-marker "#"))

    (when topic (add-text-properties 0 1 (list :body body) topic))

     (save-match-data
                     (when (and body (string-match (concat "^" header-marker "+?\s.*$")  body))
                       (setq body (with-temp-buffer
                                    (insert body)
                                    (goto-char (point-min))
                                    (while (re-search-forward (concat "^" header-marker "+?\s.*$") nil t)
                                      (replace-match (concat header-marker "\\&")))
                                    (buffer-string)))))

    (concat header-marker " " author " " (consult-gh--time-ago createdAt)
            " " (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time createdAt)) "\n"
            "-----\n" body "\n" "\n")))

(defun consult-gh--issue-filter-comments-with-query (comments &optional maxnum)
  "Filter COMMENTS when there are more than MAXNUM.

Queries the user for how to filter the comments."

  (let* ((maxnum (or maxnum consult-gh-comments-maxnum)))
    (when (and (listp comments) (> (length comments) maxnum))
      (pcase (consult--read (list (cons "Yes, Load Everything" :nil)
                                  (cons (format "No, Load up to %s latest comments." maxnum) :last-maxnum)
                                  (cons "No, let me enter the number of commetns to load" :last-number)
                                  (cons "No, only load the comments in the last week." :last-week)
                                  (cons "No, only load the comments in the last month." :last-month)

                                  (cons "No, only load the comments since a date I choose" :date)
                                  (cons "No, only load the comments in a date range I choose" :daterange))
                            :prompt (format "There are more than %s comments on that issue.  Do you want to load them all?" maxnum)
                            :lookup #'consult--lookup-cdr
                            :sort nil)
        (':last-week
         (setq comments (cl-remove-if-not (lambda (k)
                                            (time-less-p (encode-time (decoded-time-add (decode-time (current-time) t) (make-decoded-time :day -7))) (date-to-time (gethash :createdAt k))))
                                          comments)))
        (':last-month
         (setq comments (cl-remove-if-not (lambda (k)
                                            (time-less-p (encode-time (decoded-time-add (decode-time (current-time) t) (make-decoded-time :day -30))) (date-to-time (gethash :createdAt k))))
                                          comments)))
        (':last-maxnum
         (setq comments (cl-subseq comments 0 (min (length comments) maxnum))))
        (':last-number
         (let ((number (read-number "Enter the number of comments to load: ")))
               (when (numberp number)
         (setq comments (cl-subseq comments 0 (min (length comments) (truncate number)))))))
        (':date
         (let* ((limit-begin (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (gethash :createdAt (car comments)))))
                (limit-end (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (gethash :createdAt (car (last comments))))))
                (d (org-read-date nil t nil (format "Select Begin Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date)))))
           (setq comments (cl-remove-if-not (lambda (k)
                                              (time-less-p d (date-to-time (gethash :createdAt k))))
                                            comments))))
        (':daterange (let* ((limit-begin (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (gethash :createdAt (car comments)))))
                            (limit-end (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (gethash :createdAt (car (last comments))))))
                            (begin-date (org-read-date nil t nil (format "Select Begin Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date))))
                            (end-date (org-read-date nil t nil (format "Select End Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date)))))
                       (setq comments (cl-remove-if (lambda (k)
                                                          (or (time-less-p (date-to-time (gethash :createdAt k)) begin-date)
                                                                (time-less-p end-date (date-to-time (gethash :createdAt k)))))

                                                        comments))))))
    comments))

(defun consult-gh--issue-filter-comments (comments &optional maxnum)
  "Filter COMMENTS when there are more than MAXNUM.

Use `consult-gh-issues-show-comments-in-view' to decide how to filter
the comments."
  (when (and comments (listp comments))
  (pcase consult-gh-issues-show-comments-in-view
    ('all comments)
    ((pred (lambda (var) (numberp var)))
     (cl-subseq comments (max 0 (- (length comments) consult-gh-issues-show-comments-in-view)
                          (- (length comments) (or maxnum consult-gh-comments-maxnum)))))
    (_ (consult-gh--issue-filter-comments-with-query comments maxnum)))))

(defun consult-gh--issue-format-comments (comments)
  "Format the COMMENTS.

COMMENTS must be a list of hash-tables containing comment for exmplae from
`consult-gh--issue-get-comments'.

This function returns a formatted string containing the comments section
for `consult-gh--issue-view'."
  (let* ((header-marker "#")
         (out nil))
    (when (listp comments)
      (cl-loop for comment in comments
               do
               (when (hash-table-p comment)
                 (let* ((author (gethash :author comment))
                        (author (and author (gethash :login author)))
                        (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author))))
                        (authorAssociation (gethash :authorAssociation comment))
                        (authorAssociation (unless (equal authorAssociation "NONE")
                                             authorAssociation))
                        (createdAt (gethash :createdAt comment))
                        (createdAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time createdAt)))
                        (comment-url (gethash :url comment))
                        (body (gethash :body comment)))
                   (save-match-data
                     (when (and body (string-match (concat "^" header-marker " .*$")  body ))
                       (setq body (with-temp-buffer
                                    (insert body)
                                    (goto-char (point-min))
                                    (while (re-search-forward (concat "^" header-marker " +?.*$") nil t)
                                      (replace-match (concat header-marker "\\&")))
                                    (buffer-string)))))
                   (setq out (concat out
                                     (propertize
                                      (concat (and author (concat header-marker " " author " "))
                                              (and authorAssociation (concat "(" authorAssociation ")"))
                                              (and createdAt (concat (consult-gh--time-ago createdAt) " " createdAt))
                                              "\n"
                                              (and body (concat body "\n")))
                                      :consult-gh (list :author author :comment-url comment-url))))))))
    out))

(defun consult-gh--issue-comments-section (comments-text comments comments-filtered &optional preview)
  "Format the comments section with COMMENTS-TEXT.

Add a placeholder for loading the rest, when PREVIEW is non-nil or if
length of COMMENTS is larger than length of COMMENTS-FILTERED."
  (if (or preview (not consult-gh-issues-show-comments-in-view))
      (pcase consult-gh-issue-preview-major-mode
        ((or 'gfm-mode 'markdown-mode)
         (concat "\n"
                 (propertize "# " :consult-gh-issue-comments t)
                 (buttonize (propertize "Use **M-x consult-gh-issue-view-comments** to Load Comments..." :consult-gh-issue-comments t) (lambda (&rest _) (consult-gh-issue-view-comments)))
                 "\n"))
        ('org-mode
         (concat "\n"
                 (propertize "# " :consult-gh-issue-comments t)
                 (buttonize (propertize "Load Comments..." :consult-gh-issue-comments t) (lambda (&rest _) (consult-gh-issue-view-comments)))
                 "\n")))
    (cond
     ((and (listp comments) (listp comments-filtered) (> (length comments) (length comments-filtered)))
      (pcase consult-gh-issue-preview-major-mode
        ((or 'gfm-mode 'markdown-mode)
         (concat "\n"
                 (when comments-text (propertize comments-text :consult-gh-issue-comments t))
                 "\n"
                 (propertize "# " :consult-gh-issue-comments t)
                 (buttonize (propertize "Use **M-x consult-gh-issue-view-comments** to load the more..." :consult-gh-issue-comments t) (lambda (&rest _) (consult-gh-issue-view-comments)))
                 "\n"))
        ('org-mode
         (concat "\n"
                 (when comments-text (propertize comments-text :consult-gh-issue-comments t))
                 (propertize "\n" :consult-gh-issue-comments t)
                 (propertize "# " :consult-gh-issue-comments t)
                 (buttonize (propertize "Load More..." :consult-gh-issue-comments t) (lambda (&rest _) (consult-gh-issue-view-comments)))
                 "\n"))))
     (t
      (concat "\n"
              (when comments-text (propertize comments-text :consult-gh-issue-comments t))
              "\n")))))

(defun consult-gh--issue-view (repo number &optional buffer preview title)
  "Open ISSUE of REPO in an Emacs buffer, BUFFER.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and ISSUE,
a issue number of that repository, and shows
the contents of the issue in an Emacs buffer.

It fetches the preview of the ISSUE by running the command
“gh issue view ISSUE --repo REPO” using `consult-gh--call-process'
and put it as raw text in either BUFFER or if BUFFER is nil,
in a buffer named by `consult-gh-preview-buffer-name'.
If `consult-gh-issue-preview-major-mode' is non-nil, uses it as
major-mode, otherwise shows the raw text in \='fundamental-mode.

Description of Arguments:

  REPO    a string; the full name of the repository
  NUMBER  a string; issue id number
  BUFFER  a string; optional buffer name
  PREVIEW a boolean; whether to load reduced preview
  TITLE   a string; an optional title string

To use this as the default action for issues,
see `consult-gh--issue-view-action'."
  (let* ((topic (format "%s/#%s" repo number))
         (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
         (table (consult-gh--issue-read-json repo number))
         (state (gethash :state table))
         (comments (when (and consult-gh-issues-show-comments-in-view (not preview))
                      (consult-gh--issue-get-comments repo number)))
         (comments-filtered (when comments (consult-gh--issue-filter-comments comments)))

         (commenters (and table (not preview) (consult-gh--issue-get-commenters table comments)))
         (header-text (and table (consult-gh--issue-format-header repo number table topic)))
         (title (or title (car (split-string header-text "\n" t))))
         (title (string-trim-left title "title: "))
         (body-text (consult-gh--issue-format-body table topic))
         (comments-text (when (and comments-filtered (listp comments-filtered))
                           (consult-gh--issue-format-comments comments-filtered)))
         (comments-section (consult-gh--issue-comments-section comments-text comments comments-filtered preview)))

    (add-text-properties 0 1 (list :repo repo :type "issue" :commenters (mapcar (lambda (item) (concat "@" item)) commenters) :number number :title title :state state :view "issue") topic)

    (unless preview
      (consult-gh--completion-set-all-fields repo topic (consult-gh--user-canwrite repo)))

    (with-current-buffer buffer
      (let ((inhibit-read-only t))
        (erase-buffer)
        (fundamental-mode)
        (when header-text (insert header-text))
        (save-excursion
          (when (eq consult-gh-issue-preview-major-mode 'org-mode)
           (consult-gh--github-header-to-org buffer)))
        (when body-text (insert body-text))
        (when comments-section (insert comments-section))
        (consult-gh--format-view-buffer "issue")
        (outline-hide-sublevels 1)
        (consult-gh-issue-view-mode +1)
        (setq-local consult-gh--topic topic)
        (current-buffer)))))

(defun consult-gh--issue-view-action (cand)
  "Open the preview of an issue candidate, CAND.

This is a wrapper function around `consult-gh--issue-view'.
It parses CAND to extract relevant values
\(e.g. repository's name and issue number\)
and passes them to `consult-gh--issue-view'.

To use this as the default action for issues,
set `consult-gh-issue-action' to `consult-gh--issue-view-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (number (substring-no-properties (format "%s" (get-text-property 0 :number cand))))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/issues/" number "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the issue in the existing buffer." :replace)
                             (cons "Make a new buffer and load the issue in it (without killing the old buffer)." :new))
                       :prompt "You already have this issue open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))

(if existing
      (cond
       ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
       ((eq confirm :replace)
        (message "Reloading issue in the existing buffer...")
        (funcall consult-gh-switch-to-buffer-func (consult-gh--issue-view repo number existing))
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer)))
       ((eq confirm :new)
        (message "Opening issue in a new buffer...")
        (funcall consult-gh-switch-to-buffer-func (consult-gh--issue-view repo number (generate-new-buffer buffername nil)))
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--issue-view repo number))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--get-issue-templates (repo)
  "Get issue templates of REPO."
(let* ((owner (consult-gh--get-username repo))
       (package (consult-gh--get-package repo))
       (query (format "query={repository (owner: \"%s\", name: \"%s\") {issueTemplates { name
                 title
                 body
                 assignees(first:100) { nodes { login } }
                 labels(first:100) { nodes { name } }
               }}}" owner package))
       (output (consult-gh--json-to-hashtable (consult-gh--api-get-command-string "graphql" "-f" query) :data))
       (table (when (hash-table-p output)
                    (gethash :issueTemplates (gethash :repository output))))
       (templates (and table (listp table)
                         (mapcar (lambda (item) (let* ((name (gethash :name item))
                                                  (title (gethash :title item))
                                                  (body (gethash :body item))
                                                  (assignees-table (gethash :assignees item))
                                                  (assignees-list (and (hash-table-p assignees-table)
                                                              (gethash :nodes assignees-table)))
                                                  (assignees (and (listp assignees-list)
                                                                  (mapcar (lambda (item) (gethash :login item)) assignees-list)))
                                                  (labels-table (gethash :labels item))
                                                  (labels-list (and (hash-table-p labels-table)
                                                              (gethash :nodes labels-table)))
                                                  (labels (and (listp labels-list)
                                                                  (mapcar (lambda (item) (gethash :name item)) labels-list))))

                                   (cons name
                                         (list
                                          :title title
                                          :body body
                                          :assignees assignees
                                          :labels labels))))
                                 table))))
 (and templates (listp templates) (append templates (list (cons "Blank" (list :title "" :body "" :assignees (list) :labels (list))))))))

(defun consult-gh--select-issue-template (repo)
  "Select an issue template of REPO."
(let* ((templates (consult-gh--get-issue-templates repo))
       (template-name (if templates (consult--read templates
                                          :prompt "Select a template: "
                                          :require-match nil
                                          :sort t)
                        (message "repo %s does not have any issues template!" repo))))
  (and templates template-name (assoc template-name templates))))

(defun consult-gh-topics--issue-parse-metadata ()
  "Parse issue topic metadata."
  (let* ((region (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (header (when region (buffer-substring-no-properties (car region) (cdr region))))
         (assignees (when (and header (string-match ".*\\(?:\n.*assignees:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (labels (when (and header (string-match ".*\\(?:\n.*labels:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (milestone (when (and header (string-match ".*\\(?:\n.*milestone:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (projects (when (and header (string-match ".*\\(?:\n.*projects:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header))))
  (list (and (stringp assignees)
             (string-replace "\n" ", " (string-trim assignees)))
        (and (stringp labels)
             (string-replace "\n" ", " (string-trim labels)))
        (and (stringp milestone)
             (string-replace "\n" ", " (string-trim milestone)))
        (and (stringp projects)
             (string-replace "\n" ", " (string-trim projects))))))

(defun consult-gh-topics--issue-get-metadata (&optional issue)
  "Get metadata of ISSUE.

ISSUE defaults to `consult-gh--topic'."

  (let* ((issue (or issue consult-gh--topic))
         (repo (get-text-property 0 :repo issue))
         (canwrite (consult-gh--user-canwrite repo))
         (assignees (get-text-property 0 :assignees issue))
         (labels (get-text-property 0 :labels issue))
         (milestone (get-text-property 0 :milestone issue))
         (projects  (get-text-property 0 :projects issue))
         (valid-assignees (append (get-text-property 0 :assignable-users issue) (list "@me" "@copilot")))
         (valid-labels (get-text-property 0 :valid-labels issue))
         (valid-projects (get-text-property 0 :valid-projects issue))
         (valid-milestones (get-text-property 0 :valid-milestones issue)))

    (when canwrite
      (pcase-let* ((`(,text-assignees ,text-labels ,text-milestone ,text-projects) (consult-gh-topics--issue-parse-metadata)))

        (when (derived-mode-p 'org-mode)
          (setq text-assignees (or (cadar (org-collect-keywords '("assignees"))) text-assignees)
                text-labels (or (cadar (org-collect-keywords '("labels"))) text-labels)
                text-milestone (or (cadar (org-collect-keywords '("milestone"))) text-milestone)
                text-projects (or (cadar (org-collect-keywords '("projects"))) text-projects)))


        (when (stringp text-assignees)
          (setq assignees (cl-remove-duplicates
                           (cl-remove-if-not
                            (lambda (item) (member item valid-assignees))
                            (split-string text-assignees "," t "[ \t]+"))
                           :test #'equal)))

        (when (stringp text-labels)
          (setq labels (cl-remove-duplicates
                        (cl-remove-if-not
                         (lambda (item) (member item valid-labels))
                         (split-string text-labels "," t "[ \t]+"))
                        :test #'equal)))

        (when (stringp text-milestone)
          (cond
           ((member text-milestone valid-milestones)
            (setq milestone text-milestone))
           (t (setq milestone nil))))

        (when (stringp text-projects)
          (save-match-data
            (while (string-match ".*\\(?1:\".*?\"\\).*" text-projects)
              (when-let* ((p (match-string 1 text-projects)))
                 (if (member (string-trim p "\"" "\"") valid-projects)
                     (push p projects))
                (setq text-projects (string-replace p "" text-projects))))
            (setq projects (cl-remove-duplicates
                            (append projects
                                    (cl-remove-if-not
                                     (lambda (item)
                                       (member item valid-projects))
                                     (split-string text-projects "," t "[ \t]+")))
                                    :test #'equal))))))

    (list (cons "assignees" assignees)
          (cons "labels" labels)
          (cons "milestone" milestone)
          (cons "projects" projects))))

(defun consult-gh-topics--issue-create-add-metadata (&optional repo issue)
  "Add metadata to ISSUE of REPO.

This is used when creating new issues for REPO."
  (let* ((issue (or issue consult-gh--topic))
         (meta (consult-gh-topics--issue-get-metadata issue))
         (repo (or repo (get-text-property 0 :repo issue)))
         (canwrite (consult-gh--user-canwrite repo)))
    (when canwrite
      ;; add assignees
      (and (y-or-n-p "Would you like to add assignees?")
           (let* ((table (or (get-text-property 0 :assignable-users issue)
                              (consult-gh--get-assignable-users repo)))
                  (users (append (list "@me" "@copilot") table))
                  (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Users: " users)) :test #'equal)))
                  (consult-gh-topics--create-add-metadata-header "assignees" selection nil issue meta)))

           ;; add labels
           (and (y-or-n-p "Would you like to add lables?")
                (let* ((labels (or (get-text-property 0 :valid-labels issue)
                                   (consult-gh--get-labels repo)))
                       (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Labels: " labels)) :test #'equal)))

                  (consult-gh-topics--create-add-metadata-header "labels" selection nil issue meta)))

           ;; add a milestone
           (and (y-or-n-p "Would you like to change the milestone?")
                (let* ((milestones (or (get-text-property 0 :valid-milestones issue)
                                      (consult-gh--get-milestones repo)))
                       (selection (if milestones
                                      (consult--read milestones
                                                     :prompt "Select a Milestone: "
                                                     :require-match t))))
                  (if (string-empty-p selection) (setq selection nil))

                  (consult-gh-topics--create-add-metadata-header "milestone" selection t issue meta)))


           ;; add projects
           (and (y-or-n-p "Would you like to add projects?")
                (let* ((projects (or (get-text-property 0 :valid-projects issue)
                                     (consult-gh--get-projects repo)))
                       (projects (mapcar (lambda (item)
                                           (save-match-data
                                             (let ((title
                                                    (if (string-match ".*,.*" item)
                                                        (string-replace "," " - " item)
                                                      item)))
                                               (cons title item))))
                                         projects))
                       (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Projects: " projects)) :test #'equal))
                       (selection (when (listp selection)
                                    (mapcar (lambda (item) (cdr (assoc item projects))) selection)))
                       (selection
                        (when (listp selection)
                          (save-match-data
                            (mapcar (lambda (sel)
                                      (if (string-match ".*,.*" sel)
                                          (format "\"%s\"" sel)
                                        sel))
                                    selection)))))

                  (consult-gh-topics--create-add-metadata-header "projects" selection nil issue meta))))
      (setq consult-gh--topic issue)))

(defun consult-gh-topics--issue-create-submit (repo title body &optional assignees labels milestone projects web)
  "Create a new issue in REPO with TITLE and BODY.

Description of Arguments:
  REPO      a string; full name of the repository
  TITLE     a string; title of the issue
  BODY      a string; body of the issue
  ASSIGNEES a list of strings; list of assignees
  LABELS    a list of strings; list of labels
  MILESTONE a string; a milestone
  PROJECTS  a list of strings; list of projects
  WEB       a boolean; whether to continuing editing on the web?"
  (let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
         (title (or title (consult--read nil :prompt "Title: ")))
         (body (or body (consult--read nil :prompt "Body: ")))
         (assignees (if (stringp assignees)
                        assignees
                      (and assignees (listp assignees)
                           (consult-gh--list-to-string assignees))))
         (labels (if (stringp labels)
                     labels
                   (and labels (listp labels)
                        (consult-gh--list-to-string labels))))
         (projects (if (stringp projects)
                       projects
                     (and projects (listp projects)
                          (consult-gh--list-to-string projects))))
         (milestone (if (and (stringp milestone) (not (string-empty-p milestone)))
                        milestone
                      (and milestone (listp milestone)
                           (car milestone))))
         (args nil))
    (when (and title body)
      (setq args (delq nil (append args
                                   (list "--repo" repo)
                                   (list "--title" title)
                                   (list "--body" body)
                                   (and assignees (list "--assignee" assignees))
                                   (and labels (list "--label" labels))
                                   (and milestone (list "--milestone" milestone))
                                   (and projects (list "--project" projects))
                                   (and web (list "--web")))))
      (apply #'consult-gh--command-to-string "issue" "create" args))))

(defun consult-gh-topics--issue-create-presubmit (issue)
  "Prepare ISSUE to submit for creating a new issue.

ISSUE is a string with properties that identify a github issue.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh-issue-create'."
  (if consult-gh-topics-edit-mode
      (let* ((repo (get-text-property 0 :repo issue))
             (canwrite (consult-gh--user-canwrite repo))
             (nextsteps (append (list (cons "Submit" :submit))
                                (list (cons "Continue in the Browser" :browser))
                                (and canwrite (list (cons "Add/Change Metadata" :metadata)))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what to do next? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))
        (while (eq next ':metadata)
          (consult-gh-topics--issue-create-add-metadata)
          (setq next (consult--read nextsteps
                                    :prompt "Choose what to do next? "
                                    :lookup #'consult--lookup-cdr
                                    :sort nil)))
        (pcase-let* ((`(,title . ,body) (consult-gh-topics--get-title-and-body))
                     (title (or title
                                (and (derived-mode-p 'org-mode)
                                     (cadar (org-collect-keywords
                                             '("title"))))
                                ""))
                     (body (or body ""))
                     (metadata (consult-gh-topics--issue-get-metadata))
                     (assignees (cdr (assoc "assignees" metadata)))
                     (labels (cdr (assoc "labels" metadata)))
                     (milestone (cdr (assoc "milestone" metadata)))
                     (projects (cdr (assoc "projects" metadata))))

          (pcase next
            (':browser (and (consult-gh-topics--issue-create-submit repo title body assignees labels milestone projects t)))
            (':submit (and (consult-gh-topics--issue-create-submit repo title body assignees labels milestone projects nil)
                           (message "Issue Submitted!")
                           (funcall consult-gh-quit-window-func t))))))
    (message "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-restore-default (&optional issue)
  "Restore default values when editing an ISSUE."
  (if consult-gh-topics-edit-mode
  (let* ((issue (or issue consult-gh--topic))
         (repo (get-text-property 0 :repo issue))
         (canwrite (consult-gh--user-canwrite repo))
         (title (get-text-property 0 :original-title issue))
         (body (get-text-property 0 :original-body issue))
         (assignees (get-text-property 0 :original-assignees issue))
         (labels (get-text-property 0 :original-labels issue))
         (milestone (get-text-property 0 :original-milestone issue))
         (projects (get-text-property 0 :original-projects issue))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))

    (add-text-properties 0 1 (list :title title :assignees assignees :labels labels :milestone milestone :projects projects) issue)

    (save-excursion
      ;; change title
      (goto-char (point-min))
      (when (re-search-forward "^.*title: \\(?1:.*\\)?" nil t)
        (replace-match (get-text-property 0 :title issue) nil nil nil 1))

      (when canwrite
        ;;change assignees
        (goto-char (car header))
        (when (re-search-forward "^.*assignees: \\(?1:.*\\)?" (cdr header) t)
          (replace-match (mapconcat #'identity (get-text-property 0 :assignees issue) ", ") nil nil nil 1))

        ;; change labels
        (goto-char (car header))
        (when (re-search-forward "^.*labels: \\(?1:.*\\)?" (cdr header) t)
          (replace-match (mapconcat #'identity (get-text-property 0 :labels issue) ", ") nil nil nil 1))

        ;; change projects
        (goto-char (car header))
        (when (re-search-forward "^.*projects: \\(?1:.*\\)?" (cdr header) t)
          (replace-match (mapconcat #'identity (get-text-property 0 :projects issue) ", ") nil nil nil 1)))

      ;; change milestone
      (if (equal milestone nil) (setq milestone ""))
      (goto-char (car header))
      (when (re-search-forward "^.*milestone: \\(?1:.*\\)?" (cdr header) t)
        (replace-match (get-text-property 0 :milestone issue) nil nil nil 1))

      ;; change body
      (goto-char (cdr header))
      (delete-region (point) (point-max))
      (insert body)))
(error "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-change-title (&optional new old issue)
  "Change title of ISSUE from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((issue (or issue consult-gh--topic))
         (new (or new (consult--read nil
                                     :initial old
                                     :prompt "New Title: ")))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
    (add-text-properties 0 1 (list :title new) issue)

    (when (stringp new)
      (save-excursion (goto-char (point-min))
                      (when (re-search-forward "^.*title: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                        (replace-match (get-text-property 0 :title issue) nil nil nil 1)))))
    (error "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-change-body (&optional new old issue)
  "Change body of ISSUE from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((issue (or issue consult-gh--topic))
             (new (or new (consult--read nil
                                     :initial old
                                     :prompt "New Body: ")))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))

    (when (and (stringp new) (not (string-empty-p new)))
      (add-text-properties 0 1 (list :body new) issue)

      (save-excursion
        (goto-char (cdr header))
        (delete-region (point) (point-max))
        (insert new))))
    (error "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-change-assignees (&optional new old issue)
  "Change assignees of ISSUE from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((issue (or issue consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (valid-assignees (get-text-property 0 :assignable-users issue))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (old (cond ((stringp old) old)
                        ((and (listp old) (length> old 1)) (mapconcat #'identity old sep))
                        ((and (listp old) (length< old 2)) (car old))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (new (or new
                      (if (and valid-assignees (listp valid-assignees))
                          (completing-read-multiple "Select Asignees: " valid-assignees nil t old)
                        (error "No assignable users found")))))

        (when (listp new)
          (setq new (cl-remove-duplicates
                     (cl-remove-if-not (lambda (item) (member item valid-assignees)) new) :test #'equal))
          (add-text-properties 0 1 (list :assignees new) issue)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*assignees: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :assignees issue) ", ") nil nil nil 1)))))
    (error "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-change-labels (&optional new old issue)
  "Change labels of ISSUE from OLD to NEW."
(if consult-gh-topics-edit-mode
  (let* ((issue (or issue consult-gh--topic))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (valid-labels (get-text-property 0 :valid-labels issue))
         (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
         (old (cond ((stringp old) old)
                    ((and (listp old) (length> old 1)) (mapconcat #'identity old sep))
                    ((and (listp old) (length< old 2)) (car old))))
         (old (if (and (stringp old) (not (string-suffix-p sep old)))
                  (concat old sep)
                old))
         (new (or new
                  (if (and valid-labels (listp valid-labels))
                      (completing-read-multiple "Select Labels: " valid-labels nil t old)
                    (error "No labels found!")))))

    (when (listp new)
      (setq new (cl-remove-duplicates
                 (cl-remove-if-not (lambda (item) (member item valid-labels)) new) :test #'equal))
      (add-text-properties 0 1 (list :labels new) issue)

      (save-excursion (goto-char (car header))
                      (when (re-search-forward "^.*labels: \\(?1:.*\\)?" (cdr header) t)
                        (replace-match (mapconcat #'identity (get-text-property 0 :labels issue) ", ") nil nil nil 1)))))
(error "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-change-projects (&optional new old issue)
  "Change projects of ISSUE from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((issue (or issue consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (valid-projects (get-text-property 0 :valid-projects issue))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (projects (mapcar (lambda (item)
                                 (save-match-data
                                   (let ((title
                                          (if (string-match (format ".*%s.*" sep) item)
                                              (string-replace sep " - " item)
                                            item)))
                                     (cons title item))))
                               valid-projects))
             (old (save-match-data (cond
                                    ((stringp old)
                                     (if (string-match (format ".*%s.*" sep) old)
                                         (let ((p (string-trim old "\"" "\"")))
                                           (when (member p valid-projects)
                                             (string-replace sep " - " p)))
                                              old))
                                    ((and old (listp old) (length> old 1))
                                     (mapconcat (lambda (item)
                                                  (if (string-match (format ".*%s.*" sep) item)
                                                      (let ((p (string-trim item "\"" "\"")))
                                                        (when (member p valid-projects)
                                                          (string-replace sep " - " p)))
                                                    item))
                                                old sep))
                                    ((and old (listp old) (length< old 2) (stringp (car old)))
                                     (if (string-match (format ".*%s.*" sep) (car old))
                                         (let ((p (string-trim (car old) "\"" "\"")))
                                           (when (member p valid-projects)
                                             (string-replace sep " - " p)))
                                       (car old)))
                                    (t nil))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (old (if (and (stringp old) (string-prefix-p sep old))
                      (string-remove-prefix sep old)
                    old))
             (new (or new
                      (if (and projects (listp projects))
                          (completing-read-multiple "Select Projects: " projects nil t old)
                        (error "No projects found!"))))
             (new (when (listp new)
                    (mapcar (lambda (item) (cdr (assoc item projects))) new)))
             (new
              (when (listp new)
                (save-match-data
                  (mapcar (lambda (sel)
                            (if (string-match ".*,.*" sel)
                                (format "\"%s\"" sel)
                              sel))
                          new)))))

        (when (listp new)
          (setq new (cl-remove-duplicates
                     (cl-remove-if-not (lambda (item)
                                         (or (member item valid-projects)
                                             (member (string-trim item "\"" "\"") valid-projects)))
                                       new)
                     :test #'equal))

          (add-text-properties 0 1 (list :projects new) issue)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*projects: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :projects issue) ", ") nil nil nil 1)))))
    (error "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit-change-milestone (&optional new old issue)
  "Change milestone of ISSUE from OLD to NEW."
  (if consult-gh-topics-edit-mode
  (let* ((issue (or issue consult-gh--topic))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (valid-milestones (get-text-property 0 :valid-milestones issue))
         (new (or new
                  (if (and valid-milestones (listp valid-milestones))
                      (consult--read valid-milestones
                                     :initial old
                                     :prompt "Milestone: "
                                     :require-match t)
                    (error "No milestones found!")))))

    (when (stringp new)
      (if (string-empty-p new) (setq new nil))
      (add-text-properties 0 1 (list :milestone new) issue)
      (save-excursion (goto-char (car header))
                      (when (re-search-forward "^.*milestone: \\(?1:.*\\)?" (cdr header) t)
                        (replace-match (get-text-property 0 :milestone issue) nil nil nil 1)))))
(error "Not in an issue editing buffer!")))

(defun consult-gh-topics--edit-issue-submit (issue &optional title body assignees labels milestone projects)
  "Edit ISSUE with new metadata.

Description of Arguments:
  ISSUE     a plis: list of key value pairs for issue
  TITLE     a string; new title
  BODY      a string; new body
  ASSIGNEES a list of strings; new list of assignees
  LABELS    a list of strings; new list of labels
  PROJECTS  a list of strings; new list of projects
  MILESTONE a string; new milestone"

  (pcase-let* ((repo (or (get-text-property 0 :repo issue) (get-text-property 0 :repo (consult-gh-search-repos nil t))))
               (canwrite (consult-gh--user-canwrite repo))
               (token-scopes (consult-gh--auth-get-token-scopes))
               (number (or (get-text-property 0 :number issue)  (get-text-property 0 :number (consult-gh-issue-list repo t))))
               (original-title (get-text-property 0 :original-title issue))
               (original-body (get-text-property 0 :original-body issue))
               (original-assignees (get-text-property 0 :original-assignees issue))
               (original-labels (get-text-property 0 :original-labels issue))
               (original-projects (get-text-property 0 :original-projects issue))
               (original-milestone (get-text-property 0 :original-milestone issue))
               (`(,add-assignees ,remove-assignees)                          (consult-gh--separate-add-and-remove assignees original-assignees))
               (`(,add-labels ,remove-labels)                          (consult-gh--separate-add-and-remove labels original-labels))
               (`(,add-projects ,remove-projects)                          (consult-gh--separate-add-and-remove projects original-projects))
               (add-milestone (and (not (equal milestone original-milestone)) (stringp milestone) milestone))
               (remove-milestone (and (not (equal milestone original-milestone)) (or (equal milestone nil) (string-empty-p milestone))))
               (title (and (not (equal title original-title)) title))
               (body (and (not (equal body original-body)) body))
               (args (list "--repo" repo)))


    (when (and add-assignees (listp add-assignees)) (setq add-assignees (consult-gh--list-to-string add-assignees)))

    (when (and remove-assignees (listp remove-assignees)) (setq remove-assignees (consult-gh--list-to-string remove-assignees)))

    (when (and add-labels (listp add-labels))
      (setq add-labels  (consult-gh--list-to-string add-labels)))

    (when (and remove-labels (listp remove-labels))
      (setq remove-labels (consult-gh--list-to-string remove-labels)))

    (when (and add-projects (listp add-projects))
      (setq add-projects (consult-gh--list-to-string add-projects)))

    (when (and remove-projects (listp remove-projects))
      (setq remove-projects (consult-gh--list-to-string remove-projects)))


    (when (or (and canwrite (or title body add-assignees remove-assignees add-labels remove-labels add-milestone remove-milestone add-projects remove-projects))
              (or title body))
      (setq args (delq nil (append args
                                   (and title (list "--title" (concat (substring-no-properties title))))
                                   (and body (list "--body" (concat (substring-no-properties body) )))
                                   (and canwrite add-assignees (list "--add-assignee" add-assignees))
                                   (and canwrite remove-assignees (list "--remove-assignee" remove-assignees))
                                   (and canwrite add-labels (list "--add-label" add-labels))
                                   (and canwrite remove-labels (list "--remove-label" remove-labels))
                                   (and canwrite (member "project" token-scopes) add-projects (list "--add-project" add-projects))
                                   (and canwrite (member "project" token-scopes) remove-projects (list "--remove-project" remove-projects))
                                   (and canwrite add-milestone (list "--milestone" (concat (substring-no-properties add-milestone))))
                                   (and canwrite remove-milestone (list "--remove-milestone")))))

      (apply #'consult-gh--command-to-string "issue" "edit" number args))))

(defun consult-gh-topics--edit-issue-presubmit (issue)
  "Prepare edits on ISSUE to submit.

ISSUE is a string with properties that identify a github issue.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--issue-view'."
  (if consult-gh-topics-edit-mode
      (let* ((repo (get-text-property 0 :repo issue))
             (canwrite (consult-gh--user-canwrite repo))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (isAuthor (consult-gh--user-isauthor issue))
             (nextsteps (if (or canwrite isAuthor)
                            (append (list (cons "Submit" :submit))
                                    (and canwrite
                                         (list (cons "Add/Remove Assignees" :assignees) (cons "Add/Remove Labels" :labels) (cons "Change Milestone" :milestone) (cons "Add/Remove Projects" :projects)))
                                    (list (cons "Change Title" :title))
                                    (list (cons "Change Body" :body))
                                    (list (cons "Discard edits and restore original values" :default))
                                    (list (cons "Cancel" :cancel)))
                          (user-error "Current user, %s, does not have permissions to edit this pull request" user)))
             (next (consult--read nextsteps
                                  :prompt "Choose what do you want to do? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))

        (pcase-let* ((`(,title . ,body) (consult-gh-topics--get-title-and-body))
                     (title (or title
                                (and (derived-mode-p 'org-mode)
                                     (cadar (org-collect-keywords
                                             '("title"))))
                                ""))
                     (body (or body ""))
                     (metadata (when canwrite (consult-gh-topics--issue-get-metadata)))
                     (assignees (when metadata (cdr (assoc "assignees" metadata))))
                     (labels (when metadata (cdr (assoc "labels" metadata))))
                     (milestone (when canwrite (cdr (assoc "milestone" metadata))))
                     (projects (when canwrite (cdr (assoc "projects" metadata)))))


          (pcase next
            (':default (consult-gh-issue--edit-restore-default))
            (':title (consult-gh-issue--edit-change-title nil title))
            (':body  (consult-gh-issue--edit-change-body nil nil))
            (':assignees (consult-gh-issue--edit-change-assignees nil assignees))
            (':labels (consult-gh-issue--edit-change-labels nil labels))
            (':milestone (consult-gh-issue--edit-change-milestone nil milestone))
            (':projects (consult-gh-issue--edit-change-projects nil projects))
            (':submit
             (and (consult-gh-topics--edit-issue-submit issue title body assignees labels milestone projects)

                  (message "Edits Submitted!")
                  (funcall consult-gh-quit-window-func t))))))
    (message "Not in an issue editing buffer!")))

(defun consult-gh-issue--edit (&optional issue)
  "Edit ISSUE.

Creates a buffer to edit ISSUE's title body, and etc."
  (let* ((issue (or issue consult-gh--topic))
         (isAuthor (consult-gh--user-isauthor issue))
         (repo (get-text-property 0 :repo issue))
         (canwrite (consult-gh--user-canwrite repo))
         (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (_ (if (not (or canwrite isAuthor))
                (message "The current user, %s, %s to edit this issue" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
         (number (get-text-property 0 :number issue))
         (title (get-text-property 0 :title issue))
         (body (get-text-property 0 :body issue))
         (assignees (get-text-property 0 :assignees issue))
         (labels (get-text-property 0 :labels issue))
         (projects (get-text-property 0 :projects issue))
         (milestone (get-text-property 0 :milestone issue))
         (buffer (format "*consult-gh-issue-edit: %s #%s" repo number))
         (newtopic (format "%s/#%s" repo number))
         (type "issue"))

       (if canwrite
           ;; collect valid projects for completion at point
           (consult-gh--completion-set-valid-projects newtopic repo)
         (add-text-properties 0 1 (list :valid-projects nil) newtopic))

       (add-text-properties 0 1 (text-properties-at 0 issue) newtopic)
       (add-text-properties 0 1 (list :isComment nil :type type :new nil :original-title title :original-body body :original-assignees assignees :original-labels labels :original-milestone milestone :original-projects projects) newtopic)

       (with-timeout (1 nil)
         (while (not (plist-member (text-properties-at 0 issue) :valid-labels))
           (sit-for 0.01)))
       (add-text-properties 0 1 (text-properties-at 0 issue) newtopic)

;; insert buffer contents
     (consult-gh-topics--insert-buffer-contents (consult-gh-topics--get-buffer-create buffer "issue" newtopic) newtopic :title title :body body :assignees assignees :labels labels :milestone milestone :projects projects :canwrite canwrite)

       (funcall consult-gh-pop-to-buffer-func buffer)))

(defun consult-gh--pr-list-format (string input highlight)
  "Format minibuffer candidates for listing pull requests.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh pr list ...”\)
  INPUT     the query from the user
            \(a.k.a. command line argument passed to the gh call\)
  HIGHLIGHT if non-nil, input is highlighted
            with `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "pr")
         (type "pr")
         (parts (string-split string "\t"))
         (repo (car (consult--command-split input)))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (number (car parts))
         (state (upcase (cadr (cdr (cdr parts)))))
         (face (pcase state
                 ("CLOSED" 'consult-gh-error)
                 ("MERGED" 'consult-gh-success)
                 ("OPEN" 'consult-gh-repo)
                 (_ 'consult-gh-pr)))
         (branch (cadr (cdr parts)))
         (headbranch (save-match-data
                       (if
                           (string-match "\\(?1:.*?\\):\\(?2:.*\\)" branch)
                         (match-string 2 branch)
                         branch)))
         (headrepo (save-match-data
                       (if
                           (string-match "\\(?1:.*?\\):\\(?2:.*\\)" branch)
                         (concat (match-string 1 branch) "/" package)
                         repo)))
         (title (cadr parts))
         (date (cadr (cdr (cdr (cdr parts)))))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width (concat (propertize (format "%s" number) 'face  face) ":" (propertize (format "%s" title) 'face 'consult-gh-default)) 70)
                      (propertize (consult-gh--set-string-width state 6) 'face face)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize (consult-gh--set-string-width branch 24) 'face 'consult-gh-branch)
                      (consult-gh--set-string-width (concat (propertize user 'face 'consult-gh-user ) "/" (propertize package 'face 'consult-gh-package)) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :baserepo repo :user user :package package :number number :state state :title title :ref branch :headbranch headbranch :headrepo headrepo :date date :query query :class class :type type) str)
    str))

(defun consult-gh--search-prs-format (string input highlight)
  "Format minibuffer candidates for searching pull requests.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh search prs ...”\)
  INPUT     the query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
            with `consult-gh-highlight-match' in the minibuffer."

  (let* ((class "pr")
         (type "pr")
         (parts (string-split string "\t"))
         (repo (car parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (number (cadr parts))
         (state (upcase (cadr (cdr parts))))
         (face (pcase state
                 ("CLOSED" 'consult-gh-error)
                 ("MERGED" 'consult-gh-success)
                 ("OPEN" 'consult-gh-repo)
                 (_ 'consult-gh-pr)))
         (title (cadr (cdr (cdr parts))))
         (tags (cadr (cdr (cdr (cdr parts)))))
         (date (cadr (cdr (cdr (cdr (cdr parts))))))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width (concat (propertize (format "%s" number) 'face  face) ":" (propertize (format "%s" title) 'face 'consult-gh-default)) 70)
                      (propertize (consult-gh--set-string-width state 6) 'face face)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (propertize (consult-gh--set-string-width tags 18) 'face 'consult-gh-tags)
                      (consult-gh--set-string-width (concat (propertize user 'face 'consult-gh-user ) "/" (propertize package 'face 'consult-gh-package)) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :number number :state state :title title :tags tags :date date :query query :class class :type type) str)
    str))

(defun consult-gh--pr-state ()
  "State function for pull request candidates.

This is passed as STATE to `consult--read' in `consult-gh-search-prs'
and is used to preview or do other actions on the pr."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (if cand
          (pcase action
            ('preview
             (if (and consult-gh-show-preview cand)
                 (when-let* ((repo (get-text-property 0 :repo cand))
                            (number (get-text-property 0 :number cand))
                            (query (get-text-property 0 :query cand))
                            (match-str (consult--build-args query))
                            (buffer (get-buffer-create consult-gh-preview-buffer-name)))
                   (add-to-list 'consult-gh--preview-buffers-list buffer)
                   (consult-gh--pr-view repo number buffer t)
                   (with-current-buffer buffer
                     (if consult-gh-highlight-matches
                         (cond
                          ((listp match-str)
                           (mapc (lambda (item)
                                     (highlight-regexp item 'consult-gh-preview-match)) match-str))
                          ((stringp match-str)
                           (highlight-regexp match-str 'consult-gh-preview-match)))))
                   (funcall preview action
                            buffer))))
            ('return
             cand))))))

(defun consult-gh--pr-list-group (cand transform)
  "Group function for pull requests.

This is passed as GROUP to `consult--read' in `consult-gh-pr-list'
or `consult-gh-search-prs', and is used to group prs.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-prs-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Number:Title " 68 nil ?-)
       (consult-gh--set-string-width " State " 8 nil ?-)
       (consult-gh--set-string-width " Date " 12 nil ?-)
       (consult-gh--set-string-width " Branch " 26 nil ?-)
       (consult-gh--set-string-width " Repo " 40 nil ?-))))))

(defun consult-gh--pr-search-group (cand transform)
  "Group function for pull requests.

This is passed as GROUP to `consult--read' in `consult-gh-pr-list'
or `consult-gh-search-prs', and is used to group prs.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-prs-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Number:Title " 68 nil ?-)
       (consult-gh--set-string-width " State " 8 nil ?-)
       (consult-gh--set-string-width " Date " 12 nil ?-)
       (consult-gh--set-string-width " Tags " 20 nil ?-)
       (consult-gh--set-string-width " Repo " 40 nil ?-))))))

(defun consult-gh--pr-browse-url-action (cand)
  "Browse the url for a pull request candidate, CAND.

This is an internal action function that gets a candidate, CAND,
from `consult-gh-search-prs' and opens the url of the pr
in an external browser.

To use this as the default action for prs,
set `consult-gh-pr-action' to `consult-gh--pr-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (number (substring-no-properties (get-text-property 0 :number cand)))
         (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")))
         (url (and repo-url (concat repo-url "/pull/" number))))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--pr-read-json (repo number &optional json)
  "Get json response of pull request of NUMBER in REPO.

Runs an async shell command with the command:
gh pr view NUMBER --repo REPO --json JSON
and returns the output as a hash-table.

If optional argument PREVIEW is non-nil, do not load full details.
Optional argument JSON, defaults to `consult-gh--pr-view-json-fields'."
  (let* ((json-object-type 'hash-table)
         (json-array-type 'list)
         (json-key-type 'keyword)
         (json-false :false))
    (json-read-from-string (consult-gh--command-to-string "pr" "view" number "--repo" repo "--json" (or json consult-gh--pr-view-json-fields)))))

(defun consult-gh--pr-get-comments (repo number)
  "Get comments and reviews of pull request NUMBER in REPO.

Retrieves a list of comments and reviews for pull request stored in TABLE,
a hash-table output from `consult-gh--pr-read-json'."
  (let* ((comments (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/issues/%s/comments" repo number))))
         (reviews (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/pulls/%s/reviews" repo number))))
         (review-comments (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/pulls/%s/comments" repo number))))
         (all-comments (append comments reviews review-comments)))
    (if (< emacs-major-version 30) ;;temp for backward compatibility
        (sort all-comments (lambda (x y)
                               (let ((x_date (date-to-time (or (gethash :updated_at x) (gethash :created_at x) (gethash :submitted_at x) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t))))))
                                 (y_date (date-to-time (or (gethash :updated_at y) (gethash :created_at y) (gethash :submitted_at y) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                 (if (time-less-p x_date y_date) t nil))))
    (sort all-comments :key (lambda (k)
                              (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (gethash :submitted_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t))))))))))

(defun consult-gh--pr-get-commenters (table &optional comments)
  "Get list of relevant users on a pull request.

Retrieves a list of all relevant users (commenters, reviewers, etc.) for
a pull request stored in TABLE, a hash-table output from
`consult-gh--pr-read-json'.

If the optinal argument COMMENTS is non-nil,
use COMMENTS instead of the comments in the table."
  (let* ((author (gethash :login (gethash :author table)))
         (assignees (gethash :assignees table))
         (assignees (and (listp assignees) (mapcar (lambda (item) (and (hash-table-p item) (gethash :login item))) assignees)))
         (comments (or comments (gethash :comments table)))
         (commenters (when (and comments (listp comments)) (cl-loop for comment in comments
                                                    collect
                                                    (when (hash-table-p comment)
                                                       (gethash :login (gethash :user comment)))))))
    (cl-remove-duplicates (delq nil (append (list author) assignees commenters)) :test #'equal)))

(defun consult-gh--pr-format-header (repo number table &optional topic)
  "Format a header for a pull reqeust of NUMBER in REPO.

TABLE is a hash-table output containing pull request information
from `consult-gh--pr-read-json'.  Returns a formatted string containing
the header section for `consult-gh--pr-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--pr-view'."
  (let* ((title (gethash :title table))
         (author (gethash :login (gethash :author table)))
         (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author) 'rear-nonsticky t)))
         (baseRef (gethash :baseRefName table))
         (head (gethash :headRepository table))
         (headRepo (and head (gethash :name head)))
         (headRepoOwner (gethash :headRepositoryOwner table))
         (headRepoOwner (and headRepoOwner (gethash :login headRepoOwner)))
         (headRef (gethash :headRefName table))
         (state (gethash :state table))
         (createdAt (gethash :createdAt table))
         (updatedAt (gethash :updatedAt table))
         (updatedAt (and updatedAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time updatedAt))))
         (html-url (gethash :url table))
         (closedAt (gethash :closedAt table))
         (closedAt (and closedAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time closedAt))))

         (labels (gethash :labels table))
         (labels (and labels (listp labels) (mapcar (lambda (item)
                                                              (when (hash-table-p item)
                                                                (let* ((name (gethash :name item))
                                                                       (desc (gethash :description item))
                                                                       (color (gethash :color item)))
                                                                  (when (stringp name)
                                                                    (propertize name 'face `(t :background ,(concat "#" color) :box (:color ,(concat "#" color) :line-width (-1 . -2))) 'help-echo (apply-partially #'consult-gh--get-label-tooltip name desc color) 'rear-nonsticky t)))))
                                                    labels)))
         (labels-text (and labels (listp labels) (mapconcat #'identity labels "\s\s")))
         (milestone (gethash :milestone table))
         (milestone-title (and (hash-table-p milestone) (gethash :title milestone)))
         (milestone-text (and (hash-table-p milestone) (propertize milestone-title 'help-echo (concat (format "%s\n%s"
                                                                                                              (or (gethash :title milestone) "")
                                                                                                              (or (gethash :description milestone) "no description")))
                                                                   'rear-nonsticky t)))
         (reviewers (gethash :reviewRequests table))
          (reviewers (and reviewers (listp reviewers) (mapcar (lambda (item)
                                                              (when (hash-table-p item)
                                                                (let* ((login (gethash :login item)))
                                                                  (when (stringp login)
                                                                    (propertize login 'help-echo (apply-partially #'consult-gh--get-user-tooltip login) 'rear-nonsticky t)))))
                                                              reviewers)))
         (reviewers-text (and reviewers (listp reviewers) (mapconcat #'identity
                                                                     reviewers ",\s")))
         (assignees (gethash :assignees table))
         (assignees (and assignees (listp assignees) (mapcar (lambda (item)
                                                                (when (hash-table-p item)
                                                                (let* ((login (gethash :login item)))
                                                                  (if (stringp login)
                                                                    (propertize login 'help-echo (apply-partially #'consult-gh--get-user-tooltip login) 'rear-nonsticky t)
                                                                    login))))
                                                             assignees)))
         (assignees-text (and assignees (listp assignees) (mapconcat #'identity assignees ",\s")))
         (projects (gethash :projectItems table))
         (projects (and projects (listp projects) (mapcar (lambda (item) (when (hash-table-p item) (gethash :title item))) projects)))
         (projects-text (and projects (listp projects) (mapconcat #'identity projects ",\s"))))

    (when (stringp topic)
      (add-text-properties 0 1 (list :author author :state state :lastUpdated updatedAt
                                     :created createdAt :closed closedAt :labels labels :milestone milestone-title :reviewers reviewers :assignees assignees :projects projects :headrepo (concat headRepoOwner "/" headRepo) :headbranch headRef :baserepo repo :basebranch baseRef :url html-url) topic))

    (concat "title: " title "\n"
            "author: " author "\n"
            "repository: " (propertize repo 'help-echo (apply-partially #'consult-gh--get-repo-tooltip repo)) "\n"
            "number: " number "\n"
            "ref: " repo ":" baseRef " <- " (and headRepoOwner (concat headRepoOwner "/")) (and headRepo (format "%s:" headRepo)) headRef "\n"
            "state: " state "\n"
            (and createdAt (concat "created: " createdAt "\n"))
            (and updatedAt (concat "lastUpdated: " updatedAt "\n"))
            (and closedAt (concat "closed: " closedAt "\n"))
            (and html-url (concat "url: " html-url "\n"))
            (and labels-text (concat "labels: " "[ " labels-text " ]""\n"))
            (and projects-text (concat "projects: " projects-text "\n"))
            (and milestone-text (concat "milestone: " milestone-text "\n"))
            (and assignees-text (concat "assignees: " assignees-text "\n"))
            (and reviewers-text (concat "reviewers: " reviewers-text "\n"))
            "\n--\n")))

(defun consult-gh--pr-format-body (table &optional topic)
  "Format the body section for a pull request.

TABLE is a hash-table output containing pull request information
from `consult-gh--pr-read-json'.  Returns a formatted string containing
the comments section for `consult-gh--pr-view'.

If optional argument PREVIEW is non-nil, do not load all comments.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--pr-view'."
  (let* ((author (gethash :login (gethash :author table)))
         (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author))))
         (body (gethash :body table))
         (createdAt (gethash :createdAt table))
         (html-url (gethash :url table))
         (header-marker "#"))
    (save-match-data
      (when (and body (string-match (concat "^" header-marker "+?\s.*$")  body))
        (setq body (with-temp-buffer
                     (insert body)
                     (goto-char (point-min))
                     (while (re-search-forward (concat "^" header-marker "+?\s.*$") nil t)
                       (replace-match (concat header-marker header-marker "\\&")))
                     (buffer-string)))))

    (when topic (add-text-properties 0 1 (list :body body) topic))

    (propertize (concat "\n## " author " " (consult-gh--time-ago createdAt)
                        " " (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time createdAt)) "\n"
                        "\n" body "\n" "\n-----\n")
                :consult-gh (list :author author
                                  :url html-url))))

(defun consult-gh--pr-format-files-changed (table)
  "Format a changed files section for a pull request.

TABLE is a hash-table output containing pull request information
from `consult-gh--pr-read-json'.  Returns a formatted string containing the
files changed section for `consult-gh--pr-view'."
  (let ((files (gethash :files table))
        (additions (gethash :additions table))
        (deletions (gethash :deletions table)))
    (and (listp files) (concat "# Files Changed - " (format "%s file(s)" (length files)) (and additions (format ", %s additions(+)" additions)) (and deletions (format ", %s deletions(-)" deletions)) "\n"))))

(defun consult-gh--pr-format-diffs (diffs repo number &optional commit-id header-level url preview)
  "Format DIFFS from pull request NUMBER in REPO.

This is used for preparing the text section of diffs in `consult-gh--pr-view'.

Description of Arguments:
  DIFFS         a string; diff text for pull request
  REPO          a string; full name of repository
  NUMBER        a string; number id of the pull request
  COMMIT-ID     a string; commit id of pull request
  HEADER-LEVEL  a number; outline level for adding the diff section
  URL           a string; html url of the commit
  PREVIEW       a boolean; whether this is for preview"
  (when (listp diffs)
    (with-temp-buffer
      (cl-loop for chunk in diffs
           do (when (consp chunk)
                   (insert (propertize (concat (make-string (+ header-level 1) ?#) " " (car-safe chunk)) :consult-gh (list :repo repo :number number :path (get-text-property 0 :path (car-safe chunk)) :commit-id commit-id :commit-url url :file t :code nil)))
                   (if preview
                       (insert "\n")
                     (when (cdr chunk)
                       (let ((start (point)))
                     (insert (concat (propertize  "\n```diff\n" :consult-gh (list :repo repo :number number :path (get-text-property 0 :path (car-safe chunk)) :commit-id commit-id :commit-url url :file t :code nil)) (propertize (cdr chunk) :consult-gh (list :repo repo :number number :path (get-text-property 0 :path (car-safe chunk)) :commit-id commit-id :commit-url url :file t :code t)) (propertize  "\n```\n" :consult-gh (list :repo repo :number number :path (get-text-property 0 :path (car-safe chunk)) :commit-id commit-id :commit-url url :file t :code nil))))
                     (save-excursion
                       (while (re-search-backward "^\s?\\*+\s\\|^\s?#\\+" start t)
                              (replace-match (apply #'propertize (concat  "," (match-string 0)) (text-properties-at 0 (match-string 0))) nil t))))))))
      (consult-gh--whole-buffer-string))))

(defun consult-gh--pr-read-filter-comments-with-query (comments &optional maxnum)
  "Filter COMMENTS when there are more than MAXNUM.

Queries the user for how to filter the comments."
  (let ((maxnum (or maxnum consult-gh-comments-maxnum)))
    (when (and (listp comments) (> (length comments) maxnum))
      (pcase (consult--read (list (cons "Yes, Load Everything" :nil)
                                  (cons (format "No, Load up to %s latest comments." maxnum) :last-maxnum)
                                  (cons "No, let me enter the number of commetns to load" :last-number)
                                  (cons "No, only load the comments in the last week." :last-week)
                                  (cons "No, only load the comments in the last month." :last-month)

                                  (cons "No, only load the comments since a date I choose" :date)
                                  (cons "No, only load the comments in a date range I choose" :daterange))
                            :prompt (format "There are more than %s comments on that pull request. Do you want to load them all?" maxnum)
                            :lookup #'consult--lookup-cdr
                            :sort nil)
        (':last-week
         (setq comments (cl-remove-if-not (lambda (k)
                                            (time-less-p (encode-time (decoded-time-add (decode-time (current-time) t) (make-decoded-time :day -7))) (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (gethash :submitted_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                          comments)))
        (':last-month
         (setq comments (cl-remove-if-not (lambda (k)
                                            (time-less-p (encode-time (decoded-time-add (decode-time (current-time) t) (make-decoded-time :day -30))) (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (gethash :submitted_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                          comments)))
        (':last-maxnum
         (setq comments (cl-subseq comments (max 0 (- (length comments) maxnum)))))
        (':last-number
         (let ((number (read-number "Enter the number of comments to load: ")))
           (when (numberp number)
             (setq comments (cl-subseq comments 0 (min (length comments) (truncate number)))))))
        (':date
         (let* ((limit-begin (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car comments)) (gethash :created_at (car comments)) (gethash :submitted_at (car comments))))))
               (limit-end (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car (last comments))) (gethash :created_at (car (last comments))) (gethash :submitted_at (car (last comments)))))))
               (d (org-read-date nil t nil (format "Select Begin Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date)))))
           (setq comments (cl-remove-if-not (lambda (k)
                                              (time-less-p d (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (gethash :submitted_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                            comments))))
        (':daterange
         (let* ((limit-begin (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car comments)) (gethash :created_at (car comments)) (gethash :submitted_at (car comments)) ))))
               (limit-end (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time (or (gethash :updated_at (car (last comments))) (gethash :created_at (car (last comments))) (gethash :submitted_at (car (last comments)))))))
               (begin-date (org-read-date nil t nil (format "Select Begin Date - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date))))
               (end-date (org-read-date nil t nil (format "Select End Date range - between %s and %s" (propertize limit-begin 'face 'consult-gh-date) (propertize limit-end 'face 'consult-gh-date)))))
           (setq comments (cl-remove-if-not (lambda (k)
                                              (let ((date (date-to-time (or (gethash :updated_at k) (gethash :created_at k) (gethash :submitted_at k) (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))))
                                                (and (time-less-p begin-date date) (time-less-p date end-date))))
                                            comments))))))
    comments))

(defun consult-gh--pr-filter-comments (comments &optional maxnum)
  "Filter COMMENTS when there are more than MAXNUM.

Queries the user for how to filter the comments."
  (when (and comments (listp comments))
  (pcase consult-gh-prs-show-comments-in-view
    ('all comments)
    ((pred (lambda (var) (numberp var)))
     (cl-subseq comments (max 0 (- (length comments) consult-gh-prs-show-comments-in-view)
                          (- (length comments) (or maxnum consult-gh-comments-maxnum)))))
    (_ (consult-gh--pr-read-filter-comments-with-query comments maxnum)))))

(defun consult-gh--pr-format-comments (comments repo number &optional url)
  "Format the COMMENTS for the pull request NUMBER in REPO.

The optional argument URL, is the web url for the pull request on GitHub."
  (let* ((header-marker "#")
         (out nil))
    (when (listp comments)
      (cl-loop for comment in comments
               do
               (when (hash-table-p comment)
                 (let* ((author (gethash :user comment))
                        (author (and author (gethash :login author)))
                        (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author))))
                        (comment-url (gethash :html_url comment))
                        (pull-url (gethash :pull_request_url comment))
                        (issue-url (gethash :issue_url comment))
                        (pull-id (gethash :pull_request_review_id comment))
                        (reply (gethash :in_reply_to_id comment))
                        (id (gethash :id comment))
                        (authorAssociation (gethash :authorAssociation comment))
                        (authorAssociation (unless (equal authorAssociation "NONE")
                                             authorAssociation))
                        (createdAt (or (gethash :updated_at comment)
                                       (gethash :created_at comment)
                                       (gethash :submitted_at comment)
                                       (format-time-string "%Y-%m-%dT%T%Z" (encode-time (decode-time (current-time) t)))))
                        (createdAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time createdAt)))
                        (state (gethash :state comment))
                        (state (cond
                                ((equal state "COMMENTED") (propertize "COMMENTED" 'face 'default))
                                ((equal state "CHANGES_REQUESTED") (propertize "REQUESTED CHANGES" 'face 'consult-gh-warning))
                                ((equal state "APPROVED") (propertize "APPROVED" 'face 'consult-gh-success))
                                (t state)))
                        (oid (gethash :commit_id comment))
                        (diff (gethash :diff_hunk comment))
                        (path (gethash :path comment))
                        (diff-chunks (when (and (stringp diff) (stringp path))
                                       (list (cons path (if (string-empty-p diff) nil diff)))))
                        (body (gethash :body comment)))

                   (save-match-data
                     (when (and body (string-match (concat "^" header-marker "+?\s.*$")  body))
                       (setq body (with-temp-buffer
                                    (insert body)
                                    (goto-char (point-min))
                                    (while (re-search-forward (concat "^" header-marker "+?\s.*$") nil t)
                                      (replace-match (concat header-marker header-marker "\\&")))
                                    (buffer-string)))))
                   (setq out (concat out (propertize (concat header-marker header-marker (when diff header-marker) " "
                                                             (and author (concat (cond
                                                                   (reply "[reply] ")
                                                                   ((and pull-url pull-id)
                                                                      "[review-comment] ")
                                                                   (issue-url "[comment] ")
                                                                   (t "[review] "))
                                                                                 author (and state (concat " " state))  " "))
                                                             (and authorAssociation (concat "(" authorAssociation ")"))
                                                             (and createdAt (concat (consult-gh--time-ago createdAt) " " createdAt))
                                                             "\n"

(and oid pull-id (concat "\ncommit: " "[" (substring oid 0 6) "]" (format "(%s/commits/%s)" url oid) "\n-----"))
(and body (concat "\n" body "\n"))
                                                             (when diff-chunks
                                                               (consult-gh--pr-format-diffs diff-chunks repo number oid 3  (format "%s/commits" url)))
                                                             "\n----------\n")
                                                     :consult-gh (list :author author :comment-url comment-url :comment-id (when (and pull-id (not (equal id pull-id))) id) :reply-url (when (and pull-id pull-url
                  (not (equal pull-id id)))
         (concat pull-url "/comments"))
                                                                       :commit-url (when (and pull-url oid) (format "%s/commits/%s" url oid))))))))))
    out))

(defun consult-gh--pr-comments-section (comments-text comments comments-filtered &optional preview)
  "Format the comments section with COMMENTS-TEXT.

Add a placeholder for loading the rest, when PREVIEW is non-nil or if
length of COMMENTS is larger than length of COMMENTS-FILTERED."
  (if (or preview (not consult-gh-prs-show-comments-in-view))
      (pcase consult-gh-issue-preview-major-mode
        ((or 'gfm-mode 'markdown-mode)
         (concat "\n"
                 (propertize "## " :consult-gh-pr-comments t)
                 (buttonize (propertize "Use **M-x consult-gh-pr-view-comments** to Load Comments..." :consult-gh-pr-comments t) (lambda (&rest _) (consult-gh-pr-view-comments)))
                 "\n"))
        ('org-mode
         (concat "\n"
                 (propertize "## " :consult-gh-pr-comments t)
                 (buttonize (propertize "Load Comments..." :consult-gh-pr-comments t) (lambda (&rest _) (consult-gh-pr-view-comments)))
                 "\n")))
    (cond
     ((and (listp comments) (listp comments-filtered) (> (length comments) (length comments-filtered)))
      (pcase consult-gh-issue-preview-major-mode
        ((or 'gfm-mode 'markdown-mode)
         (concat "\n"
                 (when (stringp comments-text) (propertize comments-text :consult-gh-pr-comments t))
                 "\n"
                 (propertize "## " :consult-gh-pr-comments t)
                 (buttonize (propertize "Use **M-x consult-gh-pr-view-comments** to load the more..." :consult-gh-pr-comments t) (lambda (&rest _) (consult-gh-pr-view-comments)))
                 "\n"))
        ('org-mode
         (concat "\n"
                 (when (stringp comments-text) (propertize comments-text :consult-gh-pr-comments t))
                 (propertize "\n" :consult-gh-pr-comments t)
                 (propertize "## " :consult-gh-pr-comments t)
                 (buttonize (propertize "Load More..." :consult-gh-pr-comments t) (lambda (&rest _) (consult-gh-pr-view-comments)))
                 "\n"))))
     (t
      (concat "\n"
              (when (stringp comments-text) (propertize comments-text :consult-gh-pr-comments t))
              "\n")))))

(defun consult-gh--pr-format-commits (commits repo number url &optional preview)
  "Format COMMITS for the pull request NUMBER in REPO.

The optional argument URL, is the web url for the pull request on GitHub.

If optional argument PREVIEW is non-nil, do not load diff of commits."
  (when (listp commits)
    (let ((out nil))
      (cl-loop for commit in commits
               do
               (when (hash-table-p commit)
                 (let* ((oid (gethash :oid commit))
                        (authors (gethash :authors commit))
                        (authors (and (listp authors) (mapconcat (lambda (author) (gethash :login author)) authors ",\s")))
                        (date (gethash :committedDate commit))
                        (date (and (stringp date) (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time date))))
                        (messageHeadline (gethash :messageHeadline commit))
                        (messageBody (gethash :messageBody commit))
                        (diff (unless preview (and oid
                                   (consult-gh--command-to-string "api" "-H" "Accept:application/vnd.github.diff" (format "repos/%s/commits/%s" repo oid)))))
                        (diff-chunks (and (stringp diff) (consult-gh--parse-diff diff)))
                        (title (propertize (concat "## " (concat "[" (substring oid 0 6) "]" (format "(%s/commits/%s)" url oid) " by " authors " at " date "\n" (when messageHeadline (concat messageHeadline "\n")) (when messageBody (concat messageBody "\n")))) :consult-gh (list :commit-id oid)))
                        (body (when (and diff-chunks (listp diff-chunks))
                                  (consult-gh--pr-format-diffs diff-chunks repo number oid 2 (format "%s/commits/%s" url oid) preview))))
                   (setq out (concat out title body)))))
      out)))

(defun consult-gh--pr-view-get-commits (&optional pr)
  "Load commits text of PR on demand."
  (let* ((pr (or pr consult-gh--topic))
         (repo (get-text-property 0 :repo pr))
         (number (get-text-property 0 :number pr))
         (_ (message "Contacting %s API..." (propertize "GitHub" 'face 'consult-gh-date)))
         (table (consult-gh--pr-read-json repo number))
         (_ (message "Loading Commits..."))
         (commits (gethash :commits table))
         (url (gethash :url table))
         (_ (message "Formatting the %s..." (propertize "content" 'face 'consult-gh-issue)))
         (commit-text
          (when (and commits (listp commits)) (consult-gh--pr-format-commits commits repo number url))))
    (when commit-text
      (with-temp-buffer (insert commit-text
                                "\n")
         (consult-gh--format-view-buffer "pr")
        (propertize (buffer-string) :consult-gh-pr-commits t)))))

(defun consult-gh--pr-commits-section (commits-text)
"Format the commits section with COMMITS-TEXT."
(or commits-text
  (pcase consult-gh-issue-preview-major-mode
    ((or 'gfm-mode 'markdown-mode)
     (concat (buttonize (propertize "Use **M-x consult-gh-pr-view-commits** to load commits diff..." :consult-gh-pr-commits t) (lambda (&rest _) (consult-gh-pr-view-commits)))
             "\n"))
    ('org-mode
     (concat (buttonize (propertize "Load Commits..." :consult-gh-pr-commits t) (lambda (&rest _) (consult-gh-pr-view-commits)))
             "\n")))))

(defun consult-gh--pr-view-get-file-changes (&optional pr)
  "Load file diff text of PR on demand."
  (let* ((pr (or pr consult-gh--topic))
         (repo (get-text-property 0 :repo pr))
         (number (get-text-property 0 :number pr))
         (_ (message "Contacting %s API..." (propertize "GitHub" 'face 'consult-gh-date)))
         (table (consult-gh--pr-read-json repo number))
         (url (gethash :url table))
         (latest-commit (gethash :headRefOid table))
         (_ (message "Loading File Changes..."))
         (diff (consult-gh--command-to-string "pr" "diff" number "--repo" repo))
         (chunks (when (and diff (stringp diff)) (consult-gh--parse-diff diff)))
         (_ (message "Formatting the %s..." (propertize "content" 'face 'consult-gh-issue)))
         (diff-text (when (and chunks (listp chunks)) (consult-gh--pr-format-diffs chunks repo number latest-commit 1 (format "%s/commits/%s" url latest-commit) nil))))
    (when diff-text
      (with-temp-buffer
        (insert diff-text
                "\n")
         (save-excursion
           (pcase consult-gh-issue-preview-major-mode
             ('gfm-mode
              (gfm-mode)
              (when (display-images-p)
                (markdown-display-inline-images)))
             ('markdown-mode
              (markdown-mode)
              (when (display-images-p)
                (markdown-display-inline-images)))
             ('org-mode
              (let ((org-display-remote-inline-images 'download))
                (consult-gh--markdown-to-org)))
             (_
              (consult-gh--markdown-to-org-emphasis)
              (outline-mode))))
         (goto-char (point-min))
         (save-excursion
           (while (re-search-forward "\r\n" nil t)
             (replace-match "\n")))
        (propertize (buffer-string) :consult-gh-pr-file-changes t)))))

(defun consult-gh--pr-file-change-section (diff-text)
"Format the file change section with DIFF-TEXT."
(if (and consult-gh-prs-show-file-changes-in-view diff-text)
             diff-text
           (pcase consult-gh-issue-preview-major-mode
             ((or 'gfm-mode 'markdown-mode)
              (concat (buttonize (propertize "Use **M-x consult-gh-pr-view-file-changes** to load file changes diff..." :consult-gh-pr-file-changes t) (lambda (&rest _) (consult-gh-pr-view-file-changes)))
                      "\n"))
             ('org-mode
              (concat (buttonize (propertize "Load File Changes..." :consult-gh-pr-file-changes t) (lambda (&rest _) (consult-gh-pr-view-file-changes)))
                      "\n")))))

(defun consult-gh--pr-view (repo number &optional buffer preview title)
  "Open the pull request with id NUMBER in REPO in BUFFER.

This is an internal function that takes REPO, the full name of a repository
\(e.g. “armindarvish/consult-gh”\) and NUMBER, a pr number of REPO,
and shows the contents of the pull request in an Emacs buffer.

It fetches the details of the pull request by calling
`consult-gh--pr-read-json' and parses and formats it in markdown syntax,
and puts it in either BUFFER, or if BUFFER is nil, in a buffer named by
`consult-gh-preview-buffer-name'.  If `consult-gh-issue-preview-major-mode'
is non-nil, uses it as major-mode for BUFFER, otherwise shows the raw text
in \='text-mode.

Description of Arguments:

  REPO    a string; the full name of the repository
  NUMBER  a string; pull request id number
  BUFFER  a string; optional buffer name
  PREVIEW a boolean; whether to load reduced preview
  TITLE   a string; an optional title string

To use this as the default action for PRs, see
`consult-gh--pr-view-action'."
  (consult--with-increased-gc
   (let* ((topic (format "%s/#%s" repo number))
          (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
          (_ (message "Collecting info from %s..." (propertize "GitHub" 'face 'consult-gh-date)))
          (table (consult-gh--pr-read-json repo number))
          (comments (when (and consult-gh-prs-show-comments-in-view (not preview))
                      (consult-gh--pr-get-comments repo number)))
          (comments-filtered (when comments (consult-gh--pr-filter-comments comments)))
          (commenters (unless preview (and table (consult-gh--pr-get-commenters table comments))))
          (state (gethash :state table))
          (url (gethash :url table))
          (_ (message "Formating the %s..." (propertize "content" 'face 'consult-gh-issue)))
          (header-text (consult-gh--pr-format-header repo number table topic))
          (title (or title (car (split-string header-text "\n" t))))
          (title (string-trim-left title "title: "))
          (latest-commit (gethash :headRefOid table))
          (body-text (consult-gh--pr-format-body table topic))
          (comments-text (when (and comments-filtered (listp comments-filtered))
                           (consult-gh--pr-format-comments comments-filtered repo number url)))
          (comments-section (consult-gh--pr-comments-section comments-text comments comments-filtered preview))
          (file-change-text (consult-gh--pr-format-files-changed table))
          (_ (message "Working on some %s details..." (propertize "more" 'face 'consult-gh-issue)))
          (diff (when consult-gh-prs-show-file-changes-in-view (consult-gh--command-to-string "pr" "diff" number "--repo" repo)))
          (chunks (when (and diff (stringp diff)) (consult-gh--parse-diff diff)))
          (diff-text (when (and chunks (listp chunks)) (consult-gh--pr-format-diffs chunks repo number latest-commit 1 (format "%s/commits/%s" url latest-commit) preview)))
          (file-change-section (consult-gh--pr-file-change-section diff-text))
          (commits (when consult-gh-prs-show-commits-in-view
                     (gethash :commits table)))
          (commits-text (when (and commits (listp commits)) (consult-gh--pr-format-commits commits repo number url preview)))
          (commits-section (consult-gh--pr-commits-section commits-text)))


     (add-text-properties 0 1 (list :repo repo :type "pr" :number number :title title :state state :commenters (mapcar (lambda (item) (concat "@" item)) commenters) :view "pr" :url url) topic)

     (unless preview
      (consult-gh--completion-set-all-fields repo topic (consult-gh--user-canwrite repo)))

     (with-current-buffer buffer
       (let ((inhibit-read-only t))
         (erase-buffer)
         (fundamental-mode)
         (when header-text (insert header-text))
         (save-excursion
           (when (eq consult-gh-issue-preview-major-mode 'org-mode)
             (consult-gh--github-header-to-org buffer)))
         (insert (concat "# Conversation\n"))
         (when body-text (insert body-text))
         ;insert comments section
         (when comments-section
           (insert comments-section))
         ;insert file changes section
         (when file-change-text (insert file-change-text))
         (when file-change-section (insert file-change-section))
         ;insert commit section
         (insert "# Commits\n")
         (when commits-section (insert commits-section))
         (message "Putting %s together..." (propertize "everything" 'face 'consult-gh-repo))
         (consult-gh--format-view-buffer "pr")
         (outline-hide-sublevels 1)
         (consult-gh-pr-view-mode +1)
         (setq-local consult-gh--topic topic)
         (current-buffer))))))

(defun consult-gh--pr-view-action (cand)
  "View a pull request candidate, CAND.

This is a wrapper function around `consult-gh--pr-view'.  It parses CAND
to extract relevant values \(e.g. repository's name and pull request
number\) and passes them to `consult-gh--pr-view'.

To use this as the default action for prs,
set `consult-gh-pr-action' to `consult-gh--pr-view-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (number (substring-no-properties (format "%s" (get-text-property 0 :number cand))))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/pull/" number "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the pull request in the existing buffer." :replace)
                             (cons "Make a new buffer and load the pull request in it (without killing the old buffer)." :new))
                       :prompt "You already have this pull request open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))
    (if existing
        (cond
         ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
         ((eq confirm :replace)
          (message "Reloading pull request in the existing buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--pr-view repo number existing))
          (set-buffer-modified-p nil)
          (buffer-name (current-buffer)))
         ((eq confirm :new)
          (message "Opening pull request in a new buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--pr-view repo number (generate-new-buffer buffername nil)))
          (set-buffer-modified-p nil)
          (buffer-name (current-buffer))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--pr-view repo number))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--pr-view-diff (repo number &optional buffer)
  "View diff of the pull request with id NUMBER in REPO in BUFFER.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and NUMBER, a pr number
of REPO, and shows the diffs of the pull request in an Emacs Buffer.

It fetches the diff from GitHub API for comparing two branches.  For
more information sww GitHub API documentation:
URL `https://docs.github.com/en/rest/commits/commits'

Description of Arguments:

  REPO    a string; the full name of the repository
  NUMBER  a string; pull request id number
  BUFFER  a string; optional buffer name

To use this as the default action for PRs, see
`consult-gh--pr-view-action'."
  (consult--with-increased-gc
   (let* ((topic (format "%s/#%s" repo number))
          (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
          (table (consult-gh--pr-read-json repo number))
          (title (gethash :title table))
          (state (gethash :state table))
          (baseRef (gethash :baseRefName table))
          (head (gethash :headRepository table))
          (headRepo (and head (gethash :name head)))
          (headRepoOwner (gethash :headRepositoryOwner table))
          (headRepoOwner (and headRepoOwner (gethash :login headRepoOwner)))
          (headRef (gethash :headRefName table))
          (diff (consult-gh--api-get-command-string (format "repos/%s/compare/%s...%s:%s" repo baseRef headRepoOwner headRef) "-H" "Accept:application/vnd.github.diff")))
     (when (stringp topic)
       (add-text-properties 0 1 (list :repo repo :type "pr" :number number :title title :state state :headrepo (concat headRepoOwner "/" headRepo) :headbranch headRef :baserepo repo :basebranch baseRef :valid-labels nil :assignable-users nil :valid-milestones nil :valid-projects nil :view "diff") topic))
     (when (and diff (stringp diff))
       (cond
        ((string-empty-p diff)
         (message "There is no diff beetwen %s and %s" (concat repo "/" baseRef) (concat headRepoOwner "/" headRepo)))
        (t
         (with-current-buffer buffer
           (erase-buffer)
           (diff-mode)
           (insert diff)
           (goto-char (point-min))
           (consult-gh-misc-view-mode +1)
           (setq-local consult-gh--topic topic)
           (current-buffer))))))))

(defun consult-gh--pr-view-diff-action (cand)
  "View diff of a pull request candidate, CAND.

This is a wrapper function around `consult-gh--pr-view-diff'.  It parses CAND
to extract relevant values \(e.g. repository's name and pull request
number\) and passes them to `consult-gh--pr-view-diff'.

To use this as the default action for prs,
set `consult-gh-pr-action' to `consult-gh--pr-view-diff-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (number (substring-no-properties (format "%s" (get-text-property 0 :number cand))))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/pull/" number " - diff"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the pull request diff in the existing buffer." :replace)
                             (cons "Make a new buffer and load the pull request diff in it (without killing the old buffer)." :new))
                       :prompt "You already have this pull request diff open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))
    (if existing
        (cond
         ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
         ((eq confirm :replace)
          (message "Reloading pull request diff in the existing buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--pr-view-diff repo number existing))
          (set-buffer-modified-p nil)
          (buffer-name (current-buffer)))
         ((eq confirm :new)
          (message "Opening pull request in a new buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--pr-view-diff repo number (generate-new-buffer buffername nil)))
          (set-buffer-modified-p nil)
          (buffer-name (current-buffer))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--pr-view-diff repo number))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--get-pr-templates (repo)
  "Get pull request templates of REPO."
  (let* ((table (consult-gh--json-to-hashtable (consult-gh--command-to-string "repo" "view" repo "--json" "pullRequestTemplates") :pullRequestTemplates))
         (templates (and table (listp table)
                         (mapcar (lambda (item) (cons (gethash :filename item)
                                                      (gethash :body item)))
                                 table))))
    (when (and templates (listp templates))
      (append templates (list (cons "Blank" ""))))))

(defun consult-gh--select-pr-template (repo)
  "Select a pull request template of REPO."
  (let* ((templates (consult-gh--get-pr-templates repo))
         (template-name (if templates (consult--read templates
                                                      :prompt "Select a template: "
                                                      :require-match nil
                                                      :sort t)
                          (message "baserepo %s does not have any pull request templates." repo))))
    (when (and templates template-name) (assoc template-name templates))))

(defun consult-gh-topics--pr-get-forks (repo &optional maxnum)
  "Get a list of forks of REPO up to MAXNUM."
  (let* ((table (consult-gh--command-to-string "api" (format "repos/%s/forks" repo) "--jq" (format "limit(%s; .[].full_name)" (or maxnum consult-gh-forks-maxnum)))))
    (when (stringp table) (split-string (string-trim table) "[\r\n]" t))))

(defun consult-gh-topics--pr-get-parent (repo)
  "Get the upstream parent of REPO."
  (let* ((table (consult-gh--json-to-hashtable (consult-gh--command-to-string "repo" "view" repo "--json" "parent")))
         (parent (and (hash-table-p table) (gethash :parent table))))
    (and (hash-table-p parent) (concat (gethash :login (gethash :owner parent)) "/" (gethash :name parent)))))

(defun consult-gh-topics--pr-get-siblings (repo)
  "Get the siblings of REPO.

Siblings here means forks of the upstream repositories."
  (let* ((current repo)
         (forks (list)))
    (while-let ((parent (consult-gh-topics--pr-get-parent repo)))
      (setq forks (append forks (consult-gh-topics--pr-get-forks parent)))
      (setq repo parent))
    (cl-remove-duplicates (delq nil (remove current (delq nil forks))) :test #'equal)))

(defun consult-gh-topics--pr-get-similar (repo)
  "Get all the similar repositories to REPO.

Similar here means forks, parents from `consult-gh-topics--pr-get-parent'
and siblings from `consult-gh-topics--pr-get-siblings'."
  (pcase consult-gh-pr-create-show-similar-repos
    ('parent (consult-gh-topics--pr-get-parent repo))
    ('forks (consult-gh-topics--pr-get-forks repo))
    ('all
     (let* ((parent (consult-gh-topics--pr-get-parent repo))
            (forks (consult-gh-topics--pr-get-forks repo))
            (siblings (consult-gh-topics--pr-get-siblings repo)))
       (cl-remove-duplicates (delq nil (append (list parent) forks siblings)) :test #'equal)))
    ('nil
     (list))))

(defun consult-gh-topics--pr-body-text-from-commits (baserepo basebranch headrepo headbranch)
"Fill the body of pull reqeust from commits.

Compares the base and head of pull request and generates a string of
commit messages to use as pull request body.

Description of Arguments:
  BASEREPO    a string; full name of the base (target) repository
  BASEBRANCH  a string; name of the base ref branch
  HEADREPO    a string; name of the head (source) repository
  HEADBRANCH  a string; name of the head ref branch"
(when-let* ((text (consult-gh--api-get-command-string (format "repos/%s/compare/%s...%s:%s" baserepo basebranch (consult-gh--get-username headrepo) headbranch) "--template" (concat "{{range .commits}}" "### (" "{{(slice .sha 0 7)}}" ")" "\s\s" "{{.commit.message}}" "\n\n" "{{end}}"))))
    (and (stringp text)
               (not (string-empty-p text))
               (concat "## Commit Messages \n" text "\n\n"))))

(defun consult-gh-topics--pr-fill-body-from-commits ()
  "Fill body of PR draft from commits."
  (let* ((pr consult-gh--topic)
         (type (get-text-property 0 :type pr))
         (new (get-text-property 0 :new pr)))
    (if (and (equal type "pr") new)
        (let* ((metadata (consult-gh-topics--pr-get-metadata))
               (baserepo (cdr (assoc "baserepo" metadata)))
               (basebranch (cdr (assoc "basebranch" metadata)))
               (headrepo (cdr (assoc "headrepo" metadata)))
               (headbranch (cdr (assoc "headbranch" metadata)))
               (commits-region (consult-gh--get-region-with-prop :consult-gh-commits-body))
               (region-beg (when (and commits-region (listp commits-region)
                                      (listp (car commits-region)))
                             (car-safe (car commits-region))))
               (commits-body (consult-gh-topics--pr-body-text-from-commits baserepo basebranch headrepo headbranch))
               (body (when (and commits-body
                                (stringp commits-body)
                                (not (string-empty-p commits-body)))
                       (propertize (pcase major-mode
                                     ('org-mode
                                      (with-temp-buffer
                                        (insert commits-body)
                                        (consult-gh--markdown-to-org)
                                        (consult-gh--whole-buffer-string)))
                                     (_ commits-body))
                                   :consult-gh-commits-body t 'rear-nonsticky t))))
          (cond
           (region-beg
            (goto-char region-beg))
           (t (goto-char (point-max))))

          (consult-gh--delete-region-with-prop :consult-gh-commits-body)

          (when body
            (if (not (looking-back "\n" (- (point) 1))) (insert "\n"))
            (insert body)))
      (message "not in a pr create draft buffer!"))))

(defun consult-gh-topics--pr-fill-body-from-template (&optional template)
  "Fill body of PR draft from a TEMPLATE."
  (let* ((pr consult-gh--topic)
         (type (get-text-property 0 :type pr))
         (new (get-text-property 0 :new pr)))
    (if (and (equal type "pr") new)
        (let* ((metadata (consult-gh-topics--pr-get-metadata))
               (baserepo (cdr (assoc "baserepo" metadata)))
               (template-region (consult-gh--get-region-with-prop :consult-gh-template-body))
               (region-beg (when (and template-region (listp template-region)
                 (listp (car template-region)))
                             (car-safe (car template-region))))
               (header-end (cdr-safe (car-safe (consult-gh--get-region-with-overlay :consult-gh-header))))
               (template (cond
                          ((consp template) template)
                          ((stringp template) (assoc template (consult-gh--get-pr-templates baserepo)))
                          (t (consult-gh--select-pr-template baserepo))))
               (template-text (and template
                                   (cdr-safe template)))
               (template-name (and template
                                   (car-safe template)))
               (body (and template-text
                          (not (string-empty-p template-text))
                          (propertize template-text :consult-gh-template-body t 'rear-nonsticky t)))
               (body (consult-gh--format-text-for-mode body)))

          (add-text-properties 0 1 (list :template template-name) pr)

          (cond
           (region-beg (goto-char region-beg))
           (header-end (goto-char header-end))
           (t (goto-char (point-max))))

          (consult-gh--delete-region-with-prop :consult-gh-template-body)

          (when body
            (if (not (looking-back "\n" (- (point) 1))) (insert "\n"))
            (insert body)))
          (message "not in a pr create draft buffer!"))))

(defun consult-gh-topics--pr-parse-metadata ()
  "Parse pull requests' metadata."
  (let* ((region (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (header (when region (buffer-substring-no-properties (car region) (cdr region))))
         (base (when (and header (string-match ".*\\(?:\n.*base:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (head (when (and header (string-match ".*\\(?:\n.*head:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (reviewers (when (and header (string-match ".*\\(?:\n.*reviewers:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (assignees (when (and header (string-match ".*\\(?:\n.*assignees:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (labels (when (and header (string-match ".*\\(?:\n.*labels:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (milestone (when (and header (string-match ".*\\(?:\n.*milestone:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header)))
         (projects (when (and header (string-match ".*\\(?:\n.*projects:\\)\\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)" header))
                      (match-string 1 header))))
  (list (and (stringp base)
              (string-replace "\n" ", " (string-trim base)))
        (and (stringp head)
              (string-replace "\n" ", " (string-trim head)))
        (and (stringp reviewers)
              (string-replace "\n" ", " (string-trim reviewers)))
        (and (stringp assignees)
              (string-replace "\n" ", " (string-trim assignees)))
        (and (stringp labels)
              (string-replace "\n" ", " (string-trim labels)))
        (and (stringp milestone)
              (string-replace "\n" ", " (string-trim milestone)))
        (and (stringp projects)
              (string-replace "\n" ", " (string-trim projects))))))

(defun consult-gh-topics--pr-get-metadata (&optional pr)
  "Get metadata of the PR.

PR defaults to `consult-gh--topic'."
  (let* ((pr (or pr consult-gh--topic))
         (baserepo (get-text-property 0 :baserepo pr))
         (basebranch (get-text-property 0 :basebranch pr))
         (headrepo (get-text-property 0 :headrepo pr))
         (headbranch (get-text-property 0 :headbranch pr))
         (canwrite (consult-gh--user-canwrite baserepo))
         (isAuthor (consult-gh--user-isauthor pr))
         (author (get-text-property 0 :author pr))
         (reviewers (get-text-property 0 :reviewers pr))
         (assignees (get-text-property 0 :assignees pr))
         (labels (get-text-property 0 :labels pr))
         (milestone (get-text-property 0 :milestone pr))
         (projects  (get-text-property 0 :projects pr))
         (valid-assignees (append (get-text-property 0 :assignable-users pr) (list "@me" "@copilot")))
         (valid-reviewers (delq author (append (get-text-property 0 :assignable-users pr) (list "@me"))))
         (valid-labels (get-text-property 0 :valid-labels pr))
         (valid-projects (get-text-property 0 :valid-projects pr))
         (valid-milestones (get-text-property 0 :valid-milestones pr))
         (valid-branches (or (get-text-property 0 :valid-refs pr)
                             (consult-gh--repo-get-branches-list baserepo t))))


    (pcase-let* ((`(,text-base ,text-head ,text-reviewers ,text-assignees ,text-labels ,text-milestone ,text-projects) (consult-gh-topics--pr-parse-metadata))
                 (text-baserepo nil)
                 (text-basebranch nil)
                 (text-headrepo nil)
                 (text-headbranch nil))

      (when (derived-mode-p 'org-mode)
        (setq text-base (or (cadar (org-collect-keywords '("base"))) text-base)
              text-head (or (cadar (org-collect-keywords '("head"))) text-head)
              text-reviewers (or (cadar (org-collect-keywords '("reviewers"))) text-reviewers)
              text-assignees (or (cadar (org-collect-keywords '("assignees"))) text-assignees)
              text-labels (or (cadar (org-collect-keywords '("labels"))) text-labels)
              text-milestone (or (cadar (org-collect-keywords '("milestone"))) text-milestone)
              text-projects (or (cadar (org-collect-keywords '("projects"))) text-projects)))

      (when (stringp text-base)
        (cond
         ((string-match "\\(?1:.*\\):\\(?2:.*\\)" text-base)
          (setq text-baserepo (string-trim (match-string 1 text-base)))
          (setq text-basebranch (string-trim (match-string 2 text-base))))
         (t
          (setq text-baserepo baserepo)
          (setq text-basebranch text-base)))

        (when (and (stringp text-baserepo) (not (string-empty-p text-baserepo)))
          (setq baserepo text-baserepo))

        (when (and (stringp text-basebranch) (not (string-empty-p text-basebranch)))
          (cond
           ((member text-basebranch valid-branches)
            (setq basebranch text-basebranch))
           (t (setq basebranch nil)))))

      (when (stringp text-head)
        (cond
         ((string-match "\\(?1:.*\\):\\(?2:.*\\)" text-head)
          (setq text-headrepo (string-trim (match-string 1 text-head)))
          (setq text-headbranch (string-trim (match-string 2 text-head))))
         (t
          (setq text-headrepo headrepo)
          (setq text-headbranch text-head)))

        (when (and (stringp text-headrepo) (not (string-empty-p text-headrepo)))
          (setq headrepo text-headrepo))

        (when (and (stringp text-headbranch) (not (string-empty-p text-headbranch)))
          (cond
           ((member text-headbranch valid-branches)
            (setq headbranch text-headbranch))
           (t (setq headbranch nil)))))

      (when (or canwrite isAuthor)
        (when (stringp text-reviewers)
          (setq reviewers (cl-remove-duplicates
                           (cl-remove-if-not
                            (lambda (item) (member item valid-reviewers))
                            (split-string text-reviewers "," t "[ \t]+"))
                           :test #'equal)))

        (when (stringp text-assignees)
          (setq assignees (cl-remove-duplicates
                           (cl-remove-if-not
                            (lambda (item) (member item valid-assignees))
                            (split-string text-assignees "," t "[ \t]+"))
                           :test #'equal)))

        (when (stringp text-labels)
          (setq labels (cl-remove-duplicates
                        (cl-remove-if-not
                         (lambda (item) (member item valid-labels))
                         (split-string text-labels "," t "[ \t]+"))
                        :test #'equal)))

        (when (stringp text-projects)
          (save-match-data
            (while (string-match ".*\\(?1:\".*?\"\\).*" text-projects)
              (when-let* ((p (match-string 1 text-projects)))
                 (if (member (string-trim p "\"" "\"") valid-projects)
                     (push p projects))
                (setq text-projects (string-replace p "" text-projects))))
            (setq projects (cl-remove-duplicates
                            (append projects
                                    (cl-remove-if-not
                                     (lambda (item)
                                       (member item valid-projects))
                                     (split-string text-projects "," t "[ \t]+")))
                                    :test #'equal))))

        (when (stringp text-milestone)
          (cond
           ((member text-milestone valid-milestones)
            (setq milestone text-milestone))
           (t (setq milestone nil))))))

    (list (cons "baserepo" baserepo)
          (cons "basebranch" basebranch)
          (cons "headrepo" headrepo)
          (cons "headbranch" headbranch)
          (cons "reviewers" reviewers)
          (cons "assignees" assignees)
          (cons "labels" labels)
          (cons "milestone" milestone)
          (cons "projects" projects))))

(defun consult-gh-topics--pr-create-add-metadata (&optional repo pr)
  "Add metadata to PR topic for REPO.

This is used for creating new pull requests."
  (let* ((pr (or pr consult-gh--topic))
         (meta (consult-gh-topics--pr-get-metadata pr))
         (repo (or repo (get-text-property 0 :repo pr)))
         (baserepo (get-text-property 0 :baserepo pr))
         (canwrite (consult-gh--user-canwrite baserepo)))
    (when canwrite
      ;; add reviewers
      (and (y-or-n-p "Would you like to add reviewers?")
           (let* ((author (get-text-property 0 :author pr))
                  (users (or (remove author (get-text-property 0 :assignable-users pr))
                             (and (not (member :assignable-users (text-properties-at 0 pr)))
                                  (delq author (consult-gh--get-assignable-users repo)))))
                  (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Users: " users)) :test #'equal)))

(consult-gh-topics--create-add-metadata-header "reviewers" selection nil pr meta)))

      ;; add assignees
      (and (y-or-n-p "Would you like to add assignees?")
           (let* ((table (or (get-text-property 0 :assignable-users pr)
                             (and (not (member :assignable-users (text-properties-at 0 pr)))
                                  (consult-gh--get-assignable-users repo))))
                  (users (append (list "@me" "@copilot") table))
                  (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Users: " users)) :test #'equal)))

(consult-gh-topics--create-add-metadata-header "assignees" selection nil pr meta)))

      ;; add labels
      (and (y-or-n-p "Would you like to add lables?")
           (let* ((labels (or (get-text-property 0 :valid-labels pr)
                              (and (not (member :valid-labels (text-properties-at 0 pr)))
                                   (consult-gh--get-labels repo))))
                  (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Labels: " labels)) :test #'equal)))

             (consult-gh-topics--create-add-metadata-header "labels" selection nil pr meta)))

          ;; add a milestone
      (and (y-or-n-p "Would you like to add a milestone?")
           (let* ((milestones (or (get-text-property 0 :valid-milestones pr)
                                  (and (not (member :valid-milestones (text-properties-at 0 pr)))
                                       (consult-gh--get-milestones repo))))
                  (selection (if milestones (consult--read milestones
                                           :prompt "Select a Milestone: "
                                           :require-match t))))
             (if (string-empty-p selection) (setq selection nil))

(consult-gh-topics--create-add-metadata-header "milestone" selection t pr meta)))

      ;; add projects
      (and (y-or-n-p "Would you like to add projects?")
           (let* ((projects (or (get-text-property 0 :valid-projects pr)
                                (and (not (member :valid-projects (text-properties-at 0 pr)))
                                     (consult-gh--get-projects repo))))
                  (projects (mapcar (lambda (item)
                                      (save-match-data
                                        (let ((title
                                               (if (string-match ".*,.*" item)
                                                   (string-replace "," " - " item)
                                                 item)))
                                      (cons title item))))
                                    projects))
                  (selection (cl-remove-duplicates (delq nil (completing-read-multiple "Select Projects: " projects)) :test #'equal))
                  (selection (when (listp selection)
                               (mapcar (lambda (item) (cdr (assoc item projects))) selection)))
                  (selection
                   (when (listp selection)
                   (save-match-data
                     (mapcar (lambda (sel)
                               (if (string-match ".*,.*" sel)
                                   (format "\"%s\"" sel)
                                 sel))
                             selection)))))

(consult-gh-topics--create-add-metadata-header "projects" selection nil pr meta))))
      (setq consult-gh--topic pr)))

(defun consult-gh-topics--pr-create-change-refs (&optional pr)
  "Change refs in PR topic.

This is used for changing ref branches in a pull requests."
  (let* ((pr (or pr consult-gh--topic))
         (meta (consult-gh-topics--pr-get-metadata pr))
         (baserepo (cdr (assoc "baserepo" meta)))
         (canwrite (consult-gh--user-canwrite baserepo))
         (isAuthor (consult-gh--user-isauthor pr))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))

    (when (or canwrite isAuthor)
      (let* ((baserepo (get-text-property 0 :repo (consult-gh-search-repos (consult-gh--get-package baserepo) t "Search for the target base repo you want to merge to: ")))
             (basebranch (consult-gh--read-branch baserepo nil "Select the branch you want to merge to: " t nil))

             (headrepo (get-text-property 0 :repo (consult-gh-search-repos (consult-gh--get-package baserepo) t "Search for the source head repo you want to merge from: ")))
             (headbranch (cond
                          ((equal baserepo headrepo)
                           (consult-gh--read-branch baserepo nil "Select the head branch: " t nil (lambda (cand) (not (equal (substring-no-properties cand) basebranch)))))
                          (t
                           (consult-gh--read-branch headrepo nil "Select the head branch: " t nil)))))

        (while (equal (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "/repos/%s/compare/%s...%s" baserepo (concat (consult-gh--get-username baserepo) ":" basebranch) (concat (consult-gh--get-username headrepo) ":" headbranch))) :status) "identical")
          (when (y-or-n-p "There is no commit between head and base refs.  Do you want to select a different head branch?")
                          (setq headbranch (cond
                          ((equal baserepo headrepo)
                           (consult-gh--read-branch baserepo nil "Select the head branch: " t nil (lambda (cand) (not (member  (substring-no-properties cand) (list basebranch headbranch))))))
                          (t
                           (consult-gh--read-branch headrepo nil "Select the head branch: " t nil (lambda (cand) (not (equal  (substring-no-properties cand) headbranch)))))))))

        (add-text-properties 0 1 (list :baserepo baserepo :basebranch basebranch :headrepo headrepo :headbranch headbranch) pr)

        ;;collect valid refs for completion at point
        (consult-gh--completion-set-pr-refs pr baserepo headrepo)

        (save-excursion (goto-char (car header))
                        (when (re-search-forward "^.*base: \\(?1:.*\\)?" (cdr header) t)
                          (replace-match (concat (get-text-property 0 :baserepo pr) ":" (get-text-property 0 :basebranch pr)) nil nil nil 1)))
        (save-excursion (goto-char (car header))
                        (when (re-search-forward "^.*head: \\(?1:.*\\)?" (cdr header) t)
                          (replace-match (concat (get-text-property 0 :headrepo pr) ":" (get-text-property 0 :headbranch pr)) nil nil nil 1)))
        (setq consult-gh--topic pr)))))

(defun consult-gh-topics--pr-create-view-diff (&optional pr refresh)
  "View diff for PR topic.

When REFRESH reload the diff in the current buffer."
  (when-let* ((pr (or pr
                 (and (stringp consult-gh--topic)
                      (equal (get-text-property 0 :type consult-gh--topic) "pr")
                      consult-gh--topic)))
         (baserepo (get-text-property 0 :baserepo pr))
         (basebranch (get-text-property 0 :basebranch pr))
         (headrepo (get-text-property 0 :headrepo pr))
         (headbranch (get-text-property 0 :headbranch pr))
         (buffername (format "*consult-gh-pr-create-diff: %s:%s<-%s:%s" baserepo basebranch headrepo headbranch))
         (ref (format "%s:%s...%s:%s" (consult-gh--get-username baserepo) basebranch (consult-gh--get-username headrepo) headbranch))
         (newtopic (format "%s/compare/%s" baserepo ref))
         (diff (consult-gh--api-get-command-string (format "repos/%s/compare/%s" baserepo ref) "-H" "Accept:application/vnd.github.diff")))
    (when (stringp diff)
      (let* ((existing (and (not refresh) (get-buffer buffername)))
             (confirm (if (and existing (not (= (buffer-size existing) 0)))
                          (consult--read
                           (list (cons "Switch to existing buffer." :resume)
                                 (cons "Reload the diff in the existing buffer." :replace)
                                 (cons "Make a new buffer and load the diff in it (without killing the old buffer)." :new))
                           :prompt "You already have this diff open in another buffer.  Would you like to switch to that buffer or make a new one? "
                           :lookup #'consult--lookup-cdr
                           :sort nil
                           :require-match t))))
        (when existing
          (cond
           ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
           (t
            (add-text-properties 0 1 (text-properties-at 0 pr) newtopic)
            (add-text-properties 0 1 (list :type "compareDiff" :ref ref) newtopic)
            (cond
             ((eq confirm :replace)
              (message "Reloading the diff in the existing buffer..."))
             ((eq confirm :new)
              (message "Opening the diff in a new buffer...")
              (setq buffername (generate-new-buffer buffername nil)))))))
        (unless (and existing (eq confirm :resume))
          (with-current-buffer (get-buffer-create buffername)
            (let ((inhibit-read-only t))
              (erase-buffer)
              (diff-mode)
              (insert diff)
              (consult-gh-misc-view-mode +1)
              (setq-local consult-gh--topic newtopic)
              (goto-char (point-min)))
            (funcall consult-gh-pop-to-buffer-func (current-buffer))))
        (current-buffer)))))

(defun consult-gh-topics--pr-create-view-format-commits (baserepo basebranch headowner headbranch)
  "Format commits comparison of two branches.

Description of Arguments:
  BASEREPO    a string; full name of the base (target) repository
  BASEBRANCH  a string; name of the base ref branch
  HEADOWNER   a string; name of the pwner of the head (source)
              repository
  HEADBRANCH  a string; name of the head ref branch"
  (let* ((json-object-type 'hash-table)
         (json-array-type 'list)
         (json-key-type 'keyword)
         (json-false :false)
         (table (json-read-from-string (consult-gh--api-get-command-string (format "repos/%s/compare/%s...%s:%s" baserepo basebranch headowner headbranch))))
         (commits (when (hash-table-p table) (gethash :commits table)))
         (out ""))
    (when (and commits (listp commits))
      (cl-loop for commit in commits
               do (let* ((oid  (gethash :sha commit))
                         (diff (consult-gh--command-to-string "api" "-H" "Accept:application/vnd.github.diff" (format "repos/%s/commits/%s" baserepo oid)))
                         (diff-chunks (and (stringp diff) (consult-gh--parse-diff diff)))
                         (commitObj (gethash :commit commit))
                         (author (and (hash-table-p commitObj) (gethash :author commitObj)))
                         (author (and (hash-table-p author) (gethash :name author)))
                         (date (and (hash-table-p commitObj) (gethash :committer commitObj)))
                         (date (and (hash-table-p date)(gethash :date date)))
                         (date (and (stringp date) (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time date))))
                         (message (and (hash-table-p commitObj) (gethash :message commitObj)))
                         (url (gethash :html_url commit))
                         (title (propertize (concat "## " (concat "[" (substring oid 0 6) "]" (format "(%s)" url) " by " author " at " date "\n" (when message (concat message "\n")))) :consult-gh (list :commit-id oid)))
                         (body
                          (when (and diff-chunks (listp diff-chunks))
                            (consult-gh--pr-format-diffs diff-chunks baserepo nil oid 2 url nil))))
                    (setq out (concat out title body))))
      out)))

(defun consult-gh-topics--pr-create-view-commits (&optional pr refresh)
  "View commits for PR topic.

When REFRESH reload the diff in the current buffer."
  (let* ((pr (or pr consult-gh--topic))
         (baserepo (get-text-property 0 :baserepo pr))
         (basebranch (get-text-property 0 :basebranch pr))
         (headrepo (get-text-property 0 :headrepo pr))
         (headbranch (get-text-property 0 :headbranch pr))
         (buffername (format "*consult-gh-pr-create-commits: %s:%s<-%s:%s" baserepo basebranch headrepo headbranch))
         (ref (format "%s:%s...%s:%s" (consult-gh--get-username baserepo) basebranch (consult-gh--get-username headrepo) headbranch))
         (newtopic (format "%s/compare/%s" baserepo ref))
         (commits-text (consult-gh-topics--pr-create-view-format-commits baserepo basebranch (consult-gh--get-username headrepo) headbranch)))
    (when (or (equal (get-text-property 0 :type pr) "pr")
              (equal (get-text-property 0 :type pr) "compareCommits"))
      (when (and commits-text (stringp commits-text))
        (let* ((existing (and (not refresh) (get-buffer buffername)))
               (confirm (if (and existing (not (= (buffer-size existing) 0)))
                            (consult--read
                             (list (cons "Switch to existing buffer." :resume)
                                   (cons "Reload the commits in the existing buffer." :replace)
                                   (cons "Make a new buffer and load the commits in it (without killing the old buffer)." :new))
                             :prompt "You already have these commits open in another buffer.  Would you like to switch to that buffer or make a new one? "
                             :lookup #'consult--lookup-cdr
                             :sort nil
                             :require-match t))))
          (when existing
            (cond
             ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
             (t
              (add-text-properties 0 1 (text-properties-at 0 pr) newtopic)
              (add-text-properties 0 1 (list :type "compareCommits" :ref ref) newtopic)
              (cond
               ((eq confirm :replace)
                (message "Reloading commits in the existing buffer..."))
               ((eq confirm :new)
                (message "Opening pull request in a new buffer...")
                (setq buffername (generate-new-buffer buffername nil)))))))
          (unless (and existing (eq confirm :resume))
            (with-current-buffer (get-buffer-create buffername)
              (let ((inhibit-read-only t))
                (erase-buffer)
                (insert (format "ref: %s:%s <- %s:%s\n\n--\n" baserepo basebranch headrepo headbranch))
                (save-excursion
                  (when (eq consult-gh-issue-preview-major-mode 'org-mode)
                    (consult-gh--github-header-to-org (current-buffer))))
                (insert "# Commits\n")
                (insert commits-text)
                (insert "\n")
                (consult-gh--format-view-buffer "pr")
                (outline-hide-sublevels 2)
                (goto-char (point-min))
                (consult-gh-misc-view-mode +1)
                (setq-local consult-gh--topic newtopic)
                (funcall consult-gh-pop-to-buffer-func (current-buffer)))))
           (current-buffer))))))

(defun consult-gh-topics--pr-create-view-format-file-changes (baserepo basebranch headowner headbranch)
  "Format commits comparison of two branches.

Description of Arguments:
  BASEREPO    a string; full name of the base (target) repository
  BASEBRANCH  a string; name of the base ref branch
  HEADOWNER   a string; name of the owner of the head (source)
              repository
  HEADBRANCH  a string; name of the head ref branch"
(let* ((json-object-type 'hash-table)
       (json-array-type 'list)
       (json-key-type 'keyword)
       (json-false :false)
       (table (json-read-from-string (consult-gh--api-get-command-string (format "repos/%s/compare/%s...%s:%s" baserepo basebranch headowner headbranch)))))
  (when (hash-table-p table)
       (let* ((latest-commit (gethash :headRefOid table))
              (url (gethash :html_url table))
              (file-change-text (consult-gh--pr-format-files-changed table))
              (diff (consult-gh--command-to-string "api" "-H" "Accept:application/vnd.github.diff" (format "repos/%s/compare/%s...%s:%s" baserepo basebranch headowner headbranch)))
              (chunks (when (and diff (stringp diff)) (consult-gh--parse-diff diff)))
              (diff-text (when (and chunks (listp chunks)) (consult-gh--pr-format-diffs chunks baserepo nil latest-commit 1 url nil))))
(when (stringp file-change-text)
  (concat file-change-text "\n" (when (stringp diff-text) diff-text)))))))

(defun consult-gh-topics--pr-create-view-file-changes (&optional pr refresh)
  "View file change for PR topic.

When REFRESH reload the diff in the current buffer."
  (let* ((pr (or pr consult-gh--topic))
         (baserepo (get-text-property 0 :baserepo pr))
         (basebranch (get-text-property 0 :basebranch pr))
         (headrepo (get-text-property 0 :headrepo pr))
         (headbranch (get-text-property 0 :headbranch pr))
         (buffername (format "*consult-gh-pr-create-file-changes: %s:%s<-%s:%s" baserepo basebranch headrepo headbranch))
         (ref (format "%s:%s...%s:%s" (consult-gh--get-username baserepo) basebranch (consult-gh--get-username headrepo) headbranch))
         (newtopic (format "%s/compare/%s" baserepo ref))
         (file-changes-text (consult-gh-topics--pr-create-view-format-file-changes baserepo basebranch (consult-gh--get-username headrepo) headbranch)))
    (when (or (equal (get-text-property 0 :type pr) "pr")
              (equal (get-text-property 0 :type pr) "compareFileChanges"))
      (when (and file-changes-text (stringp file-changes-text))
        (let* ((existing (and (not refresh) (get-buffer buffername)))
               (confirm (if (and existing (not (= (buffer-size existing) 0)))
                            (consult--read
                             (list (cons "Switch to existing buffer." :resume)
                                   (cons "Reload the file changes in the existing buffer." :replace)
                                   (cons "Make a new buffer and load the file changes in it (without killing the old buffer)." :new))
                             :prompt "You already have these file changes open in another buffer.  Would you like to switch to that buffer or make a new one? "
                             :lookup #'consult--lookup-cdr
                             :sort nil
                             :require-match t))))
          (when existing
            (cond
             ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
             (t
               (add-text-properties 0 1 (text-properties-at 0 pr) newtopic)
               (add-text-properties 0 1 (list :type "compareFileChanges" :ref ref) newtopic)
              (cond
               ((eq confirm :replace)
                (message "Reloading file changes in the existing buffer..."))
               ((eq confirm :new)
                (message "Opening file changes in a new buffer...")
                (setq buffername (generate-new-buffer buffername nil)))))))
          (unless (and existing (eq confirm :resume))
            (with-current-buffer (get-buffer-create buffername)
              (let ((inhibit-read-only t))
            (erase-buffer)
            (insert (format "ref: %s:%s <- %s:%s\n\n--\n" baserepo basebranch headrepo headbranch))
            (save-excursion
              (when (eq consult-gh-issue-preview-major-mode 'org-mode)
                (consult-gh--github-header-to-org (current-buffer))))
            (insert file-changes-text)
            (insert "\n")
            (consult-gh--format-view-buffer "pr")
            (outline-hide-sublevels 2)
            (goto-char (point-min))
            (consult-gh-misc-view-mode +1)
            (setq-local consult-gh--topic newtopic)
            (funcall consult-gh-pop-to-buffer-func (current-buffer)))))
           (current-buffer))))))

(defun consult-gh-topics--pr-create-submit (baserepo basebranch headrepo headbranch title body &optional reviewers assignees labels milestone projects draft fill web)
  "Create a new pull request in REPO with metadata.

Description of Arguments:
  BASEREPO    a string; full name of the base (target) repository
  BASEBRANCH  a string; name of the base ref branch
  HEADREPO    a string; name of the head (source) repository
  HEADBRANCH  a string; name of the head ref branch
  TITLE     a string; title of the pr
  BODY      a string; body of the pr
  REVIEWERS a list of strings; list of reviewers
  ASSIGNEES a list of strings; list of assignees
  LABELS    a list of strings; list of labels
  MILESTONE a string; a milestone
  PROJECTS  a list of strings; list of projects
  DRAFT     a boolean; whether to submit pull request as draft?
  FILL      a string; whether to add commit details?
              this can either be t, first, or verbose
  WEB       a boolean; whether to continuing editing on the web?"
  (let* ((baserepo (or baserepo (get-text-property 0 :repo (consult-gh-search-repos nil t "Select the target base repo you want to merge to: "))))
         (basebranch (or basebranch (consult-gh--read-branch baserepo nil "Select the base branch you want to merge into: " t nil)))
         (headrepo (or headrepo (get-text-property 0 :repo (consult-gh-search-repos (consult-gh--get-package baserepo) t "Select the target base repo you want to merge from: "))))
         (headbranch (or headbranch (cond
                                     ((equal baserepo headrepo)
                                      (consult-gh--read-branch baserepo nil "Select the head branch: " t nil (lambda (cand) (not (equal (substring-no-properties cand) basebranch)))))
                                     (t
                                      (consult-gh--read-branch headrepo nil "Select the head branch: " t nil)))))
         (title (or title (consult--read nil :prompt "Title: ")))
         (body (or body (consult--read nil :prompt "Body: ")))
         (head headbranch)
         (base basebranch)
         (reviewers (if (stringp reviewers)
                        reviewers
                      (and reviewers (listp reviewers)
                           (consult-gh--list-to-string reviewers))))
         (assignees (if (stringp assignees)
                        assignees
                      (and assignees (listp assignees)
                           (consult-gh--list-to-string assignees))))
         (labels (if (stringp labels)
                     labels
                   (and labels (listp labels)
                        (consult-gh--list-to-string labels))))
         (projects (if (stringp projects)
                       projects
                     (and projects (listp projects)
                          (consult-gh--list-to-string projects))))
         (milestone (if (and (stringp milestone) (not (string-empty-p milestone)))
                        milestone
                      (and milestone (listp milestone)
                           (car milestone))))
         (args nil))

    (when (not (equal baserepo headrepo))
      (setq head (concat (consult-gh--get-username headrepo) ":" headbranch)))

    (when (and baserepo head base title body)
      (setq args (delq nil (append args
                                   (list "--repo" baserepo)
                                   (list "--head" head)
                                   (list "--base" base)
                                   (list "--title" (substring-no-properties title))
                                   (list "--body" (substring-no-properties body))
                                   (and reviewers (not web) (list "--reviewer" reviewers))
                                   (and assignees (list "--assignee" assignees))
                                   (and labels (list "--label" labels))
                                   (and milestone (list "--milestone" milestone))
                                   (and projects (list "--project" projects))
                                   (and draft (and (not web) (list "--draft")))
                                   (and web (and (not draft) (list "--web")))
                                   (and fill (pcase fill
                                               ('t (list "--fill"))
                                               ("first" (list "--fill-first"))
                                               ("verbose" (list "--fill-verbose")))))))
      (apply #'consult-gh--command-to-string "pr" "create" args))))

(defun consult-gh-topics--pr-create-presubmit (pr)
  "Prepare PR to submit for creating a new pull request.

PR is a string with properties that identify a github pull requests.
For an example, see  the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh-pr-create'."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (baserepo (get-text-property 0 :baserepo pr))
             (canwrite (consult-gh--user-canwrite baserepo))
             (isAuthor (consult-gh--user-isauthor pr))
             (nextsteps (append
                                (list (cons "Submit" :submit))
                                (list (cons "Submit as Draft" :draft))
                                (list (cons "Continue in the Browser" :browser))
                                (list (cons "View Diff" :diff))
                                (list (cons "View All Commits" :commits))
                                (list (cons "View File Changes" :files))
                                (and canwrite (list (cons "Add Metadata" :metadata)))
                                (and (or canwrite isAuthor) (list (cons "Change Refs" :refs)))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what to do next? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))
        (while (eq next ':metadata)
          (consult-gh-topics--pr-create-add-metadata)
          (setq next (consult--read nextsteps
                                    :prompt "Choose what to do next? "
                                    :lookup #'consult--lookup-cdr
                                    :sort nil)))

        (while (eq next ':refs)
          (consult-gh-topics--pr-create-change-refs)
          (setq next (consult--read nextsteps
                                    :prompt "Choose what to do next? "
                                    :lookup #'consult--lookup-cdr
                                    :sort nil)))
        (cond
         ((eq next ':diff)  (consult-gh-topics--pr-create-view-diff pr))
         ((eq next ':commits) (consult-gh-topics--pr-create-view-commits pr))
         ((eq next ':files) (consult-gh-topics--pr-create-view-file-changes pr))
         (t
          (pcase-let* ((`(,title . ,body) (consult-gh-topics--get-title-and-body))
                     (title (or title
                                (and (derived-mode-p 'org-mode)
                                     (cadar (org-collect-keywords
                                             '("title"))))
                                ""))
                     (body (or body ""))
                     (metadata (consult-gh-topics--pr-get-metadata))
                     (baserepo (cdr (assoc "baserepo" metadata)))
                     (basebranch (cdr (assoc "basebranch" metadata)))
                     (headrepo (cdr (assoc "headrepo" metadata)))
                     (headbranch (cdr (assoc "headbranch" metadata)))
                     (reviewers (cdr (assoc "reviewers" metadata)))
                     (assignees (cdr (assoc "assignees" metadata)))
                     (labels (cdr (assoc "labels" metadata)))
                     (milestone (cdr (assoc "milestone" metadata)))
                     (projects (cdr (assoc "projects" metadata))))

          (pcase next
            (':browser (and (consult-gh-topics--pr-create-submit baserepo basebranch headrepo headbranch title body reviewers assignees labels milestone projects nil nil t)))
            (':submit (and (consult-gh-topics--pr-create-submit baserepo basebranch headrepo headbranch title body reviewers assignees labels milestone projects nil nil nil)
                           (message "Pull Request Submitted!")
                           (funcall consult-gh-quit-window-func t)))
            (':draft (and (consult-gh-topics--pr-create-submit baserepo basebranch headrepo headbranch title body reviewers assignees labels milestone projects t nil nil)
                          (message "Draft Submitted!")
                          (funcall consult-gh-quit-window-func t))))))))
    (message "Not in a pull requests editing buffer!")))

(defun consult-gh-pr--edit-restore-default (&optional pr)
  "Restore default values when editing PR."

  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (repo (get-text-property 0 :repo pr))
             (canwrite (consult-gh--user-canwrite repo))
             (basebranch (get-text-property 0 :original-basebranch pr))
             (baserepo (get-text-property 0 :original-baserepo pr))
             (headrepo (get-text-property 0 :original-headrepo pr))
             (headbranch (get-text-property 0 :original-headbranch pr))
             (title (get-text-property 0 :original-title pr))
             (body (get-text-property 0 :original-body pr))
             (reviewers (get-text-property 0 :original-reviewers pr))
             (assignees (get-text-property 0 :original-assignees pr))
             (labels (get-text-property 0 :original-labels pr))
             (milestone (get-text-property 0 :original-milestone pr))
             (projects (get-text-property 0 :original-projects pr))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))

        (add-text-properties 0 1 (list :title title :baserepo baserepo :basebranch basebranch :headrepo headrepo :headbranch headbranch :reviewers reviewers :assignees assignees :labels labels :milestone milestone :projects projects) pr)

        (save-excursion
          ;; change title
          (goto-char (point-min))
          (when (re-search-forward "^.*title: \\(?1:.*\\)?" nil t)
            (replace-match (get-text-property 0 :title pr) nil nil nil 1))

          ;; change base branch
          (goto-char (point-min))
          (when (re-search-forward "^.*base: \\(?1:.*\\)?" nil t)
            (replace-match (get-text-property 0 :basebranch pr) nil nil nil 1))


          (when canwrite
            ;;change reviewers
            (goto-char (car header))
            (when (re-search-forward "^.*reviewers: \\(?1:.*\\)?" (cdr header) t)
              (replace-match (mapconcat #'identity (get-text-property 0 :reviewers pr) ", ") nil nil nil 1))

            ;;change assignees
            (goto-char (car header))
            (when (re-search-forward "^.*assignees: \\(?1:.*\\)?" (cdr header) t)
              (replace-match (mapconcat #'identity (get-text-property 0 :assignees pr) ", ") nil nil nil 1))

            ;; change labels
            (goto-char (car header))
            (when (re-search-forward "^.*labels: \\(?1:.*\\)?" (cdr header) t)
              (replace-match (mapconcat #'identity (get-text-property 0 :labels pr) ", ") nil nil nil 1))

            ;; change milestone
            (if (equal milestone nil) (setq milestone ""))
            (goto-char (car header))
            (when (re-search-forward "^.*milestone: \\(?1:.*\\)?" (cdr header) t)
              (replace-match (get-text-property 0 :milestone pr) nil nil nil 1))

            ;; change projects
            (goto-char (car header))
            (when (re-search-forward "^.*projects: \\(?1:.*\\)?" (cdr header) t)
              (replace-match (mapconcat #'identity (get-text-property 0 :projects pr) ", ") nil nil nil 1)))

          ;; change body
          (goto-char (cdr header))
          (delete-region (point) (point-max))
          (insert body)))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-title (&optional new old pr)
  "Change title of PR from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (new (or new (consult--read nil
                                         :initial old
                                         :prompt "New Title: ")))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))
        (add-text-properties 0 1 (list :title new) pr)
        (when (stringp new)
          (save-excursion (goto-char (point-min))
                          (when (re-search-forward "^.*title: \\(?1:.*\\)?" (and header (consp header) (cdr header)) t)
                            (replace-match (get-text-property 0 :title pr) nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-body (&optional new old pr)
  "Change body of PR from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (new (or new (consult--read nil
                                         :initial old
                                         :prompt "New Body: ")))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header))))

        (when (and (stringp new) (not (string-empty-p new)))
          (add-text-properties 0 1 (list :body new) pr)
          (save-excursion
            (goto-char (cdr header))
            (delete-region (point) (point-max))
            (insert new))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-base (&optional new old pr)
  "Change the base branch of PR from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (baserepo (get-text-property 0 :baserepo pr))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (branches (remove old (consult-gh--repo-get-branches-list baserepo)))
             (new (or new (and branches (listp branches)
                               (consult--read branches
                                              :prompt "Select the new base branch: "
                                              :sort t)))))

        (when (and (stringp new) (not (string-empty-p new)))
          (add-text-properties 0 1 (list :basebranch new) pr)
          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*base: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (get-text-property 0 :basebranch pr) nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-reviewers (&optional new old pr)
  "Change reviewers of PR from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (author (get-text-property 0 :author pr))
             (valid-reviewers (get-text-property 0 :assignable-users pr))
             (valid-reviewers (and (listp valid-reviewers) (delq author valid-reviewers)))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (old (cond ((stringp old) old)
                        ((and old (listp old) (length> old 1)) (mapconcat #'identity old sep))
                        ((and old (listp old) (length< old 2)) (car old))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (new (or new
                      (if (and valid-reviewers (listp valid-reviewers))
                          (completing-read-multiple "Select Reviewers: " valid-reviewers nil t old)
                        (error "No assignable reviewers found")))))

        (when (listp new)
          (setq new (cl-remove-duplicates
                     (cl-remove-if-not (lambda (item) (member item valid-reviewers)) new) :test #'equal))
          (add-text-properties 0 1 (list :reviewers new) pr)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*reviewers: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :reviewers pr) ", ") nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-assignees (&optional new old pr)
  "Change assignees of PR from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (valid-assignees (get-text-property 0 :assignable-users pr))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (old (cond ((stringp old) old)
                        ((and (listp old) (length> old 1)) (mapconcat #'identity old sep))
                        ((and (listp old) (length< old 2)) (car old))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (new (or new
                      (if (and valid-assignees (listp valid-assignees))
                          (completing-read-multiple "Select Asignees: " valid-assignees nil t old)
                        (error "No assignable users found")))))

        (when (listp new)
          (setq new (cl-remove-duplicates
                     (cl-remove-if-not (lambda (item) (member item valid-assignees)) new) :test #'equal))
          (add-text-properties 0 1 (list :assignees new) pr)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*assignees: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :assignees pr) ", ") nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-labels (&optional new old pr)
  "Change PR's labels from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (valid-labels (get-text-property 0 :valid-labels pr))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (old (cond ((stringp old) old)
                        ((and (listp old) (length> old 1)) (mapconcat #'identity old sep))
                        ((and (listp old) (length< old 2)) (car old))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (new (or new
                      (if (and valid-labels (listp valid-labels))
                          (completing-read-multiple "Select Labels: " valid-labels nil t old)
                        (error "No labels found!")))))

        (when (listp new)
          (setq new (cl-remove-duplicates
                     (cl-remove-if-not (lambda (item) (member item valid-labels)) new) :test #'equal))
          (add-text-properties 0 1 (list :labels new) pr)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*labels: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :labels pr) ", ") nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-projects (&optional new old pr)
  "Change PR's labels from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (repo (get-text-property 0 :repo pr))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (valid-projects (or (get-text-property 0 :valid-projects pr)
                                 (and (not (member :valid-projects (text-properties-at 0 pr)))
                                      (consult-gh--get-projects repo))))
             (sep (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator))
             (projects (mapcar (lambda (item)
                                 (save-match-data
                                   (let ((title
                                          (if (string-match (format ".*%s.*" sep) item)
                                              (string-replace sep " - " item)
                                            item)))
                                     (cons title item))))
                               valid-projects))
             (old (save-match-data (cond
                                    ((stringp old)
                                     (if (string-match (format ".*%s.*" sep) old)
                                         (let ((p (string-trim old "\"" "\"")))
                                           (when (member p valid-projects)
                                             (string-replace sep " - " p)))
                                              old))
                                    ((and old (listp old) (length> old 1))
                                     (mapconcat (lambda (item)
                                                  (if (string-match (format ".*%s.*" sep) item)
                                                      (let ((p (string-trim item "\"" "\"")))
                                                        (when (member p valid-projects)
                                                          (string-replace sep " - " p)))
                                                    item))
                                                old sep))
                                    ((and old (listp old) (length< old 2) (stringp (car old)))
                                     (if (string-match (format ".*%s.*" sep) (car old))
                                         (let ((p (string-trim (car old) "\"" "\"")))
                                           (when (member p valid-projects)
                                             (string-replace sep " - " p)))
                                       (car old)))
                                    (t nil))))
             (old (if (and (stringp old) (not (string-suffix-p sep old)))
                      (concat old sep)
                    old))
             (old (if (and (stringp old) (string-prefix-p sep old))
                      (string-remove-prefix sep old)
                    old))
             (new (or new
                      (if (and projects (listp projects))
                          (completing-read-multiple "Select Projects: " projects nil t old)
                        (error "No projects found!"))))
             (new (when (listp new)
                    (mapcar (lambda (item) (cdr (assoc item projects))) new)))
             (new
              (when (listp new)
                (save-match-data
                  (mapcar (lambda (sel)
                            (if (string-match ".*,.*" sel)
                                (format "\"%s\"" sel)
                              sel))
                          new)))))
        (when (listp new)
          (setq new (cl-remove-duplicates
                     (cl-remove-if-not (lambda (item)
                                         (or (member item valid-projects)
                                             (member (string-trim item "\"" "\"") valid-projects)))
                                       new)
                     :test #'equal))
          (add-text-properties 0 1 (list :projects new) pr)

          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*projects: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (mapconcat #'identity (get-text-property 0 :projects pr) ", ") nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-pr--edit-change-milestone (&optional new old pr)
  "Change PR's milestone from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((pr (or pr consult-gh--topic))
             (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
             (valid-milestones (get-text-property 0 :valid-milestones pr))
             (new (or new
                      (if (and valid-milestones (listp valid-milestones))
                          (consult--read valid-milestones
                                         :initial old
                                         :prompt "Milestone: "
                                         :require-match t)
                        (error "No milestones found!")))))

        (when (stringp new)
          (if (string-empty-p new) (setq new nil))
          (add-text-properties 0 1 (list :milestone new) pr)
          (save-excursion (goto-char (car header))
                          (when (re-search-forward "^.*milestone: \\(?1:.*\\)?" (cdr header) t)
                            (replace-match (get-text-property 0 :milestone pr) nil nil nil 1)))))
    (error "Not in a pr editing buffer!")))

(defun consult-gh-topics--edit-pr-submit (pr &optional title body basebranch reviewers assignees labels milestone projects)
  "Edit PR with new metadata.

Description of Arguments:
  PR         a plis: list of key value pairs for pull request
  TITLE      a string; new title
  BODY       a string; new body
  BASEBRANCH a string; new base branch
  REVIEWERS  a list of strings; new list of reviewers
  ASSIGNEES  a list of strings; new list of assignees
  LABELS     a list of strings; new list of labels
  PROJECTS   a list of strings; new list of projects
  MILESTONE  a string; new milestone"
  (pcase-let* ((baserepo (or (get-text-property 0 :baserepo pr) (get-text-property 0 :repo (consult-gh-search-repos nil t))))
               (canwrite (consult-gh--user-canwrite baserepo))
               (isAuthor (consult-gh--user-isauthor pr))
               (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
               (_ (unless (or canwrite isAuthor)
                    (user-error "Current user, %s, does not have permissions to edit this pull request" user)))
               (token-scopes (consult-gh--auth-get-token-scopes))
               (number (or (get-text-property 0 :number pr)  (get-text-property 0 :number (consult-gh-pr-list baserepo t))))
               (original-title (get-text-property 0 :original-title pr))
               (original-body (get-text-property 0 :original-body pr))
               (original-basebranch (get-text-property 0 :original-basebranch pr))
               (original-reviewers (get-text-property 0 :original-reviewers pr))
               (original-assignees (get-text-property 0 :original-assignees pr))
               (original-labels (get-text-property 0 :original-labels pr))
               (original-projects (get-text-property 0 :original-projects pr))
               (original-milestone (get-text-property 0 :original-milestone pr))
               (`(,add-reviewers ,remove-reviewers)                          (consult-gh--separate-add-and-remove reviewers original-reviewers))
               (`(,add-assignees ,remove-assignees)                          (consult-gh--separate-add-and-remove assignees original-assignees))
               (`(,add-labels ,remove-labels)                          (consult-gh--separate-add-and-remove labels original-labels))
               (`(,add-projects ,remove-projects)                          (consult-gh--separate-add-and-remove projects original-projects))
               (add-milestone (and (not (equal milestone original-milestone)) (stringp milestone) milestone))
               (remove-milestone (and (not (equal milestone original-milestone)) (or (equal milestone nil) (string-empty-p milestone))))
               (title (and (not (equal title original-title)) title))
               (body (and (not (equal body original-body)) body))
               (basebranch (when (and (not (equal basebranch original-basebranch)) (stringp basebranch)) basebranch))
               (args (list "--repo" baserepo)))

    (when (and add-reviewers (listp add-reviewers)) (setq add-reviewers (consult-gh--list-to-string add-reviewers)))

    (when (and remove-reviewers (listp remove-reviewers)) (setq remove-reviewers (consult-gh--list-to-string remove-reviewers)))

    (when (and add-assignees (listp add-assignees)) (setq add-assignees (consult-gh--list-to-string add-assignees)))

    (when (and remove-assignees (listp remove-assignees)) (setq remove-assignees (consult-gh--list-to-string remove-assignees)))

    (when (and add-labels (listp add-labels))
      (setq add-labels  (consult-gh--list-to-string add-labels)))

    (when (and remove-labels (listp remove-labels))
      (setq remove-labels (consult-gh--list-to-string remove-labels)))

    (when (and add-projects (listp add-projects))
      (setq add-projects (consult-gh--list-to-string add-projects)))

    (when (and remove-projects (listp remove-projects))
      (setq remove-projects (consult-gh--list-to-string remove-projects)))

    (when (or (and canwrite (or title body basebranch add-assignees remove-assignees add-labels remove-labels add-milestone remove-milestone add-projects remove-projects))
              (or title body basebranch))
      (setq args (delq nil (append args
                                   (and title (list "--title" (substring-no-properties title)))
                                   (and body (list "--body" (substring-no-properties body)))
                                   (and basebranch (list "--base" (substring-no-properties basebranch)))
                                   (and canwrite add-reviewers (list "--add-reviewer" add-reviewers))
                                   (and canwrite remove-reviewers (list "--remove-reviewer" remove-reviewers))
                                   (and canwrite add-assignees (list "--add-assignee" add-assignees))
                                   (and canwrite remove-assignees (list "--remove-assignee" remove-assignees))
                                   (and canwrite add-labels (list "--add-label" add-labels))
                                   (and canwrite remove-labels (list "--remove-label" remove-labels))
                                   (and canwrite add-milestone (list "--milestone" (concat (substring-no-properties add-milestone))))
                                   (and canwrite remove-milestone (list "--remove-milestone"))
                                   (and canwrite (member "project" token-scopes) add-projects (list "--add-project" add-projects))
                                   (and canwrite (member "project" token-scopes) remove-projects (list "--remove-project" remove-projects)))))
      (apply #'consult-gh--command-to-string "pr" "edit" number args))))

(defun consult-gh-topics--edit-pr-presubmit (pr)
  "Prepare edits on PR to submit.

PR is a string with properties that identify a github pull request.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--pr-view'."
  (if consult-gh-topics-edit-mode
      (let* ((repo (get-text-property 0 :repo pr))
             (canwrite (consult-gh--user-canwrite repo))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (isAuthor (consult-gh--user-isauthor pr))
             (nextsteps (if (or canwrite isAuthor)
                            (append (list (cons "Submit" :submit))
                                    (and canwrite
                                         (list (cons "Add/Remove Assignees" :assignees) (cons "Add/Remove Labels" :labels) (cons "Add/Remove Reviewers" :reviewers) (cons "Add/Remove Labels" :labels) (cons "Add/Remove Projects" :projects) (cons "Change Milestone" :milestone)))
                                    (list (cons "Change Base Branch" :base))
                                    (list (cons "Change Title" :title))
                                    (list (cons "Change Body" :body))
                                    (list (cons "Discard edits and restore original values" :default))
                                    (list (cons "Cancel" :cancel)))
                          (user-error "Current user, %s, does not have permissions to edit this pull request" user)))
             (next (when nextsteps (consult--read nextsteps
                                                  :prompt "Choose what do you want to do? "
                                                  :lookup #'consult--lookup-cdr
                                                  :sort nil))))
        (when next
          (pcase-let* ((`(,title . ,body) (consult-gh-topics--get-title-and-body))
                       (title (or title
                                  (and (derived-mode-p 'org-mode)
                                       (cadar (org-collect-keywords
                                               '("title"))))
                                  ""))
                       (body (or body ""))
                       (metadata (when (or isAuthor canwrite) (consult-gh-topics--pr-get-metadata)))
                       (basebranch (when metadata (cdr (assoc "basebranch" metadata))))
                       (reviewers (when (and canwrite metadata) (cdr (assoc "reviewers" metadata))))
                       (assignees (when (and canwrite metadata) (cdr (assoc "assignees" metadata))))
                       (labels (when (and canwrite metadata) (cdr (assoc "labels" metadata))))
                       (milestone (when (and canwrite metadata) (cdr (assoc "milestone" metadata))))
                       (projects (when (and canwrite metadata) (cdr (assoc "projects" metadata)))))

            (pcase next
              (':default (consult-gh-pr--edit-restore-default))
              (':title (consult-gh-pr--edit-change-title nil title))
              (':body  (consult-gh-pr--edit-change-body nil nil))
              (':assignees (consult-gh-pr--edit-change-assignees nil assignees))
              (':reviewers (consult-gh-pr--edit-change-reviewers nil reviewers))
              (':labels (consult-gh-pr--edit-change-labels nil labels))
              (':milestone (consult-gh-pr--edit-change-milestone nil milestone))
              (':projects (consult-gh-pr--edit-change-projects nil projects))
              (':base (consult-gh-pr--edit-change-base nil basebranch))
              (':submit
               (and (consult-gh-topics--edit-pr-submit pr title body basebranch reviewers assignees labels milestone projects)
                    (message "Edits %s" (propertize "Submitted!" 'face 'consult-gh-success))
                    (funcall consult-gh-quit-window-func t)))))))
    (message "% in a %s buffer!" (propertize "Not" 'face 'consult-gh-error) (propertize "pull request editing" 'face 'consult-gh-error))))

(defun consult-gh-pr--merge-create-commit (pr type &optional auto admin subject body)
  "Create a merge commit message of TYPE for PR.

TYPE can be either “merge”, “rebase”, or “squash”.

Description of Arguments:
  PR      a string; string with proerties that describes pull request
  TYPE    a string; can be  “merge”, “rebase”, or “squash”
  AUTO    a boolean; whether this is for auto-merge or not
  ADMIN   a boolean; whether this merge is overriding requirements with
          admin persmissions
  SUBJECT a string; header subject of merge commit message
  BODY    a string; body of merge commit message"
  (let* ((pr (or pr consult-gh--topic))
         (repo (get-text-property 0 :repo pr))
         (number (get-text-property 0 :number pr))
         (title (get-text-property 0 :title pr))
         (headrepo (get-text-property 0 :headrepo pr))
         (baserepo (get-text-property 0 :baserepo pr))
         (head (if (equal headrepo baserepo)
                   (get-text-property 0 :headbranch pr)
                 (concat (consult-gh--get-username headrepo) "/" (get-text-property 0 :headbranch pr))))
         (subject (or subject
                      (cond
                       ((equal type "merge") (format "Merge pull request #%s from %s" number head))
                       ((equal type "squash") title)
                       (t nil))))
         (body (or body
                   (cond
                    ((equal type "merge") title)
                    ((equal type "squash") (consult-gh--command-to-string "pr" "view" number "--repo" repo "--json" "commits" "--template" "{{range .commits}}-\s**{{.messageHeadline}}**\n{{.messageBody}}\n\n{{end}}"))
                    (t nil))))
         (newtopic (format "%s/%s merge commit" repo number))
         (buffer (format "*consult-gh-pr-%s-commit: %s #%s" type repo number)))

    (add-text-properties 0 1 (text-properties-at 0 pr) newtopic)
    (add-text-properties 0 1 (list :type "merge commit" :isComment nil :new t :target type :auto auto :admin admin) newtopic)
    (with-current-buffer  (consult-gh-topics--get-buffer-create buffer "commit message" newtopic)
      (unless (not (= (buffer-size) 0))
        (pcase-let* ((inhibit-read-only t)
                     (`(,title-marker _header-marker) (consult-gh-topics--markers-for-metadata)))

          (insert (consult-gh-topics--format-field-header-string (concat title-marker "title: ")))

          (save-mark-and-excursion
            (when subject (insert subject))
            (insert "\n\n")
            (when body
              (pcase major-mode
                ('gfm-mode (insert body))
                ('markdown-mode (insert body))
                ('org-mode (insert (with-temp-buffer
                                     (insert body)
                                     (consult-gh--markdown-to-org)                                        (consult-gh--whole-buffer-string))))
                ('text-mode (insert body))))))))

    (funcall consult-gh-pop-to-buffer-func buffer)))

(cl-defun consult-gh-pr--merge-submit (pr &key merge rebase squash admin auto disable-auto delete-branch subject body)
  "Submit merge command for PR.

This runs “gh pr merge” and passes the arguments to command line arguments.
Refer to gh's manual for detailed description of each argument.

Description of Arguments:

  PR            a string; propertized text that describes a pull request
  MERGE         a boolean; whether to add the “--merge” switch
  REBASE        a boolean; whether to add the “--rebase” switch
  SQUASH        a boolean; whether to add the “--squash” switch
  ADMIN         a boolean; whether to add the “--admin” switch
  AUTO          a boolean; whether t add the “--auto” switch
  DISABLE-AUTO  a boolean; whether to add the “--disable-auto” switch
  DELETE-BRANCH a boolean; whether to add the “--delete-branch” switch
  SUBJECT       a string; subject of the merge commit message
  BODY          a string; body of the merge commit message"
  (let* ((pr (or pr consult-gh--topic))
         (repo (get-text-property 0 :repo pr))
         (number (get-text-property 0 :number pr))
         (args (list "pr" "merge" number "--repo" repo)))
    (setq args (delq nil (append args
                                 (and admin (list "--admin"))
                                 (and auto (list "--auto"))
                                 (and disable-auto (list "--disable-auto"))
                                 (and merge (list "--merge"))
                                 (and rebase (list "--rebase"))
                                 (and squash (list "--squash"))
                                 (and subject (list "--subject" subject))
                                 (and body (list "--body" body))
                                 (and delete-branch (list "--delete-branch")))))
    (apply #'consult-gh--command-to-string args)))

(defun consult-gh-pr--merge-read-commit-type (pr &optional auto admin)
  "Query the user to pick type of commit message for merging PR.

AUTO is a boolean determining whether the commit message is for enabling
auto-merge
ADMIN is a boolean, determining whether the commit message is for a merge
overriding requirements with admin permissions."
  (if auto
      (pcase (consult--read (list (cons "Yes, create an auto merge commit" :merge)
                                  (cons "Cancel" :cancel))
                            :prompt "Confirm enabling auto merge?"
                            :lookup #'consult--lookup-cdr
                            :require-match t
                            :sort nil)

        (':merge
         (pcase (consult--read (list (cons "Yes, edit the commit message" :commit)
                                     (cons "No, use default commit message" :submit)
                                     (cons "Cancel" :cancel))
                               :prompt "Do you want to edit the commit message?"
                               :lookup #'consult--lookup-cdr
                               :require-match t
                               :sort nil)
           (':cancel)
           (':commit
            (consult-gh-pr--merge-create-commit pr "merge" auto admin))
           (':submit
            (and (y-or-n-p "This will enable automerge for the pull reqeust on GitHub.  Do you want to continue?")
                 (consult-gh-pr--merge-submit pr :merge t :auto auto :admin admin))))))
    (pcase (consult--read (list (cons "Merge commit" :merge)
                                (cons "Rebase and merge" :rebase)
                                (cons "Squash and merge" :squash)
                                (cons "Cancel" :cancel))
                          :prompt "What is next?"
                          :lookup #'consult--lookup-cdr
                          :require-match t
                          :sort nil)

      (':merge
       (pcase (consult--read (list (cons "Edit the commit subject and message" :commit)
                                   (cons "Submit" :submit))
                             :prompt "What is next?"
                             :lookup #'consult--lookup-cdr
                             :require-match t
                             :sort nil)
         (':commit
          (consult-gh-pr--merge-create-commit pr "merge" auto admin))
         (':submit
          (and (y-or-n-p "This will merge the pull reqeust on GitHub.  Do you want to continue?")
               (consult-gh-pr--merge-submit pr :merge t :auto auto :admin admin)))))
      (':rebase
       (and (y-or-n-p "This will merge the pull reqeust with a rebase commit on GitHub.  Do you want to continue?")
            (consult-gh-pr--merge-submit pr :rebase t :auto auto :admin admin)))
      (':squash
       (pcase (consult--read (list (cons "Edit the commit subject and message" :commit)
                                   (cons "Submit" :submit))
                             :prompt "What is next?"
                             :lookup #'consult--lookup-cdr
                             :require-match t
                             :sort nil)
         (':commit
          (consult-gh-pr--merge-create-commit pr "squash" auto admin))
         (':submit
          (and (y-or-n-p "This will merge the pull reqeust with a squash commit on GitHub.  Do you want to continue?")
               (consult-gh-pr--merge-submit pr :squash t :auto auto :admin admin))))))))

(defun consult-gh-pr--merge-merge (pr &optional auto admin)
  "Merge PR.

PR is a propertized string describing a pull request.
For example, PR can be the text stored in the buffer-local variable
`consult-gh--topic' in a buffer created by `consult-gh--pr-view'.

If AUTO is non-nil enables auto-merge.

If ADMIN is non-nil overrides requirements with admin premissions."
  (pcase-let* ((pr (or pr consult-gh--topic))
               (repo (get-text-property 0 :repo pr))
               (number (get-text-property 0 :number pr))
               (state (get-text-property 0 :state pr))
               (mergeable  (consult-gh--command-to-string "pr" "view" number "--repo" repo "--json" "mergeable" "--template" "{{.mergeable}}"))
               (merge-state  (consult-gh--command-to-string "pr" "view" number "--repo" repo "--json" "mergeStateStatus" "--template" "{{.mergeStateStatus}}")))

    (cond
     ((not (equal state "OPEN"))
      (message "Pull request is already %s!" (downcase state)))
     ((and (equal mergeable "CONFLICTING") (not auto))
      (pcase (consult--read (list (cons "Cancel merge and go resolve the conflicts" :cancel)
                                  (cons "Enable automerge (automatically merge when conflicts are resolved and all other requirements are met)." :auto))
                            :prompt "This branch has conflicts that must be resolved before merging. What do you want to do?"
                            :lookup #'consult--lookup-cdr
                            :require-match t
                            :sort nil)
        (':cancel)
        (':auto (consult-gh-pr--merge-read-commit-type pr t))))
     ((and (equal mergeable "MERGEABLE") (equal merge-state "BLOCKED") (not auto) (not admin))
      (pcase (consult--read (list (cons "Merge without waiting for requirements to be met (bypass branch protections)." :admin)
                                  (cons "Enable automerge (automatically merge when all requirements are met)." :auto))
                            :prompt "Merging is blocked. What would you like to do?"
                            :lookup #'consult--lookup-cdr
                            :require-match t
                            :sort nil)
        (':admin (setq admin t))
        (':auto (setq auto t)))
      (consult-gh-pr--merge-read-commit-type pr auto admin))
     ((and (equal mergeable "MERGEABLE") (equal merge-state "BLOCKED") (or auto admin))
      (consult-gh-pr--merge-read-commit-type pr auto admin))
     ((and (equal mergeable "MERGEABLE") (equal merge-state "CLEAN"))
      (consult-gh-pr--merge-read-commit-type pr auto admin))
     (t (and (y-or-n-p "Something went wrong.  Do you want to open the pull request in the browser?")
             (funcall #'consult-gh-topics-open-in-browser pr))))))

(defun consult-gh-pr--merge-enable-automerge (pr)
  "Enable auto-merge for PR.

PR is a propertized string describing a pull request.
For example, PR can be the text stored in the buffer-local variable
`consult-gh--topic' in a buffer created by `consult-gh--pr-view'."
  (consult-gh-pr--merge-merge pr t))

(defun consult-gh-pr--merge-disable-automerge (pr)
  "Disable auto-merge for PR.

PR is a propertized string describing a pull request.
For example, PR can be the text stored in the buffer-local variable
`consult-gh--topic' in a buffer created by `consult-gh--pr-view'."
  (consult-gh-pr--merge-submit pr :disable-auto t))

(defun consult-gh-topics--pr-merge-presubmit (pr)
  "Prepapre PR for merging.

This runs some interactive queries to determine how to merge the pull
request, PR, which is a propertized string describing the pull request.
For example, PR can be the text stored in the buffer-local variable
`consult-gh--topic' in a buffer created by `consult-gh--pr-view'."
  (if consult-gh-topics-edit-mode
      (pcase-let* ((auto (get-text-property 0 :auto pr))
                   (admin (get-text-property 0 :admin pr))
                   (`(,subject ,body) (consult-gh-topics--get-title-and-body))
                   (subject (or subject
                                (and (derived-mode-p 'org-mode)
                                     (cadar (org-collect-keywords
                                             '("title"))))
                                ""))
                   (subject (and (stringp subject) (substring-no-properties subject)))
                   (body (and (stringp body) (substring-no-properties body)))
                   (action (get-text-property 0 :target pr)))
        (pcase action
          ("merge" (and
                    (y-or-n-p "This will merge the pull reqeust on GitHub.  Do you want to continue?")
                    (consult-gh-pr--merge-submit pr :merge t :auto auto :admin admin :subject subject :body body)
                    (message "%s Commit Submitted!" (propertize "Merge" 'face 'consult-gh-success))
                    (funcall consult-gh-quit-window-func t)))
          ("rebase" (and
                     (y-or-n-p "This will merge the pull reqeust with a rebase commit on GitHub.  Do you want to continue?")
                     (consult-gh-pr--merge-submit pr :rebase t :auto auto :admin admin :subject subject :body body)
                     (message "%s Commit Submitted!" (propertize "Rebase" 'face 'consult-gh-success))
                     (funcall consult-gh-quit-window-func t)))
          ("squash"  (and
                      (y-or-n-p "This will merge the pull reqeust with a squash commit on GitHub.  Do you want to continue?")
                      (consult-gh-pr--merge-submit pr :squash t :auto auto :admin admin :subject subject :body body)
                      (message "%s Commit Submitted!"(propertize "Squash" 'face 'consult-gh-success))
                      (funcall consult-gh-quit-window-func t)))))
    (message "%s in a %s buffer!" (propertize "Not" 'face 'consult-gh-error) (propertize "pull request editing" 'face 'consult-gh-error))))

(defun consult-gh-topics--pr-review-add-comment (&optional review)
  "Add a comment to a REVIEW.

REVIEW is a string with properties that identify a review.
For an example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh-pr-review'."
  (let* ((topic (or review consult-gh--topic))
         (view-buffer (get-text-property 0 :view-buffer topic))
         (repo (get-text-property 0 :repo topic))
         (number (get-text-property 0 :number topic))
         (target (get-text-property 0 :target topic)))
    (cond
     ((equal target "review")
      (if (and view-buffer (buffer-live-p (get-buffer view-buffer)))
          (funcall consult-gh-pop-to-buffer-func (get-buffer view-buffer))
        (funcall #'consult-gh--pr-view repo number))
      (widen)
      (outline-hide-sublevels 2)
      (goto-char (point-min))
      (re-search-forward ".*Files Changed.*\n" nil t)))))

(defun consult-gh-topics--pr-review-insert-comment-in-buffer (body info &optional buffer)
"Insert comment with BODY and metadata INFO at the end of BUFFER.

BODY is a string for body of comment
INFO is a property list with key value pairs for :path, :line, :side,
and other info that identifies comment locatoin.  This is passed to comments
field on github api.
For more informaiton see:
URL `https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#create-a-review-for-a-pull-request'."
(let* ((comment-info (append info (list :body body)))
       (path (plist-get comment-info :path))
       (line (plist-get comment-info :line))
       (startline (plist-get comment-info :startline))
       (snippet (plist-get comment-info :snippet))
       (header-marker nil)
       (block-begin nil)
       (block-end nil)
       (text nil))

  (when (and buffer (buffer-live-p buffer))
      (with-current-buffer buffer
         (let* ((inhibit-read-only t)
                (comments (cl-remove-duplicates (append (get-text-property 0 :comments consult-gh--topic) (list comment-info)) :test #'equal))
                (mode-func nil))
           (cond
            ((derived-mode-p 'gfm-mode)
             (setq header-marker "#"
                   block-begin "```"
                   block-end "```"
                   mode-func #'gfm-mode))
            ((derived-mode-p 'markdown-mode)
             (setq header-marker "#"
                   block-begin "```"
                   block-end "```"
                   mode-func #'markdown-mode))
            ((derived-mode-p 'org-mode)
             (setq header-marker "*"
                   block-begin "#+begin_src "
                   block-end "#+end_src "
                   mode-func #'org-mode))
            (t
             (setq header-marker "#"
                   block-begin "```"
                   block-end "```"
                   mode-func #'outline-mode)))

           (setq text (with-temp-buffer
                   (insert (concat header-marker header-marker " "
                            (propertize (concat
                                   "Comment"
                                   (and path (concat " on file " path))
                                   (if startline
                                        (concat " line " (number-to-string startline) (and line (concat " to " (number-to-string line))))
                                     (and line (concat " line " (number-to-string line))))
                                   "\n"
                                   (and snippet (concat block-begin "\n"
                                                        snippet
                                                        "\n" block-end "\n")))
                                       'cursor-intangible t)
                            (if (derived-mode-p 'markdown-mode) (concat (consult-gh-topics--format-field-cursor-intangible "\n-----") "\n")
                              (consult-gh-topics--format-field-cursor-intangible "\n-----\n"))
                            (and body (propertize body :consult-gh-comments-body t))
                            (if (derived-mode-p 'markdown-mode) (concat (consult-gh-topics--format-field-cursor-intangible "\n-----") "\n")
                              (consult-gh-topics--format-field-cursor-intangible "\n-----\n"))))
           (funcall mode-func)
           (concat (propertize (consult-gh--whole-buffer-string) :consult-gh-comments comment-info :consult-gh-markings t))))
      (goto-char (point-max))
      (if (not (re-search-backward (concat header-marker " " "Comments\n") nil t))
          (insert (propertize (concat header-marker " " "Comments\n") :consult-gh-markings t 'cursor-intangible t))
        (goto-char (point-max)))
      (let ((beg (point))
             (end nil))
      (when (stringp text)
        (insert text)
        (setq end (point))
        (add-text-properties beg end (list :consult-gh-markings t)))
      (cursor-intangible-mode +1)
      (goto-char (point-max))
      (add-text-properties 0 1 (list :comments comments :isComment nil) consult-gh--topic)))))))

(defun consult-gh-topics--pr-review-append-comment (&optional topic review-buffer)
  "Add the comment in TOPIC to REVIEW-BUFFER.

TOPIC is a string with properties that identify a comment.
For an example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh-topics-comment-create'
TOPIC defaults to `consult-gh--topic'.

REVIEW-BUFFER defaults to value stored in text-properties of TOPIC."
  (let* ((topic (or topic consult-gh--topic))
         (body (consult-gh--whole-buffer-string))
         (info (get-text-property 0 :comment-info topic))
         (review-buffer (or review-buffer (get-text-property 0 :review-buffer topic))))
    (if (and review-buffer (buffer-live-p (get-buffer review-buffer)))
        (progn (and (consult-gh-topics--pr-review-insert-comment-in-buffer body info (get-buffer review-buffer))
                    (message "Comment Added!")
                    (funcall consult-gh-quit-window-func t))
               (funcall consult-gh-pop-to-buffer-func (get-buffer review-buffer)))
      (error "The review buffer, %s, does not exist" review-buffer))))

(defun consult-gh-topics--pr-review-get-comments (&optional buffer)
  "Get comments on review in BUFFER.

BUFFER defaults to the current buffer."
  (with-current-buffer (or buffer (current-buffer))
                       (let* ((regions (consult-gh--get-region-with-prop :consult-gh-comments))
                              (text-comments nil)
                              (mode-func (cond
                                     ((derived-mode-p 'gfm-mode) #'gfm-mode)
                                     ((derived-mode-p 'markdown-mode) #'markdown-mode)
                                     ((derived-mode-p 'org-mode)  #'org-mode)
                                     (t #'outline-mode))))
                         (cl-loop for region in regions
                                  do
                                  (save-excursion
                                    (let* ((body-region
                                            (car-safe (consult-gh--get-region-with-prop :consult-gh-comments-body nil (car region) (cdr region))))
                                           (body (when (and body-region (listp body-region)) (string-trim (buffer-substring (car body-region) (cdr body-region)))))
                                           (info (get-text-property (car region) :consult-gh-comments)))
                                      (when (and (stringp body) (not (string-empty-p body)))
                                        (setq text-comments (append text-comments (list (plist-put info :body (with-temp-buffer (insert (substring-no-properties body)) (funcall mode-func) (consult-gh-topics--buffer-string))))))))))
                         text-comments)))

(defun consult-gh-topics--pr-review-comment-submit (&optional topic body)
  "Create a new review comment with BODY for TOPIC.

TOPIC defaults to `consult-gh--topic' and should be a string with
properties that identify a comment.  The properties should contain
:path, :line, :side, and other info that can be passed to GitHub API.
For more informaiton see GitHub API documentation:
URL `https://docs.github.com/en/rest/pulls/comments'

BODY is a string for comment text.  When BODY is nil, the value of
\=:body key stored in TOPIC properties is stored, and if that is nil as
well, the content of the current buffer is used."
  (let* ((topic (or topic consult-gh--topic))
         (review-buffer (get-text-property 0 :review-buffer topic))
         (view-buffer (get-text-property 0 :view-buffer topic))
         (target-buffer (if (or review-buffer view-buffer)
                            (get-buffer  (or review-buffer view-buffer))))
         (comment (get-text-property 0 :comment-info topic))
         (body (or body
                   (plist-get comment :body)
                   (consult-gh-topics--buffer-string)))
         (repo (plist-get comment :repo))
         (number (plist-get comment :number))
         (commit-id (plist-get comment :commit-id))
         (path (plist-get comment :path))
         (subject-type (plist-get comment :subject-type))
         (line (plist-get comment :line))
         (side (plist-get comment :side))
         (startside (plist-get comment :startside))
         (startline (plist-get comment :startline))
         (in-reply-to (plist-get comment :in-reply-to))
         (args (list "-X" "POST" "-H" "Accept: application/vnd.github+json")))

    (setq args (append args
                       (list (format "repos/%s/pulls/%s/comments" repo number))
                       (and body (list "-f" (concat "body=" body)))
                       (and commit-id (list "-f" (concat "commit_id=" commit-id)))
                       (and path (list "-f" (concat "path=" path)))
                       (and startline (list "-F" (concat "start_line=" (number-to-string startline))))
                       (and startside (list "-f" (concat "start_side=" startside)))
                       (and line (list "-F" (concat "line=" (number-to-string line))))
                       (and side (list "-f" (concat "side=" side)))
                       (and in-reply-to (list "-f" (concat "in_reply_to=" in-reply-to)))
                       (and subject-type (list "-f" (concat "subject_type=" subject-type)))))
    (let ((response (apply #'consult-gh--call-process "api" args)))
      (cond
       ((= (car response) 0)
        (let* ((table (consult-gh--json-to-hashtable (cadr response)))
               (id (and (hash-table-p table) (gethash :id table))))
          (prog2
              (message "Review Submitted!")
              (or id nil)
            (funcall consult-gh-quit-window-func t)
            (if (and target-buffer (buffer-live-p target-buffer))
                (funcall consult-gh-pop-to-buffer-func target-buffer)))))
       ((= (car response) 422)
        (let* ((table (consult-gh--json-to-hashtable (cadr response)))
               (id (and (hash-table-p table) (gethash :id table))))
          (prog2
              (message (cadr response))
              (or id nil)
            nil)))
       (t
        (message (cadr response))
        nil)))))

(defun consult-gh-topics--pr-review-submit (repo number body &optional commit-id event comments)
  "Create a new review for pull request NMBER in REPO with BODY.

Description of Arguments:
  REPO        a string; full name of the repository
  NUMBER      a string; pull request id nunber
  BODY        a string; body text of the review
  COMMIT-ID   a string; id of the commit being reviewed
  EVENT       a string;
  COMMENTS     a list of plists; each with detials of a single comment

COMMENTS should be a list of property lists, where each plist contains
:path, :startline, :startside, :line, :side, :body as needed for a comment.
For description of these parametrs refer to:
URL `https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#create-a-review-for-a-pull-request'."
  (let ((args (list "-X" "POST" "-H" "Accept: application/vnd.github+json")))
    (setq args (append args
                       (list (format "repos/%s/pulls/%s/reviews" repo number))
                       (and body (list "-F" (concat "body=" body)))
                       (and event (list "-F" (concat "event=" event)))
                       (and commit-id (list "-F" (concat "commit_id=" commit-id)))
                       (and comments (apply #'append (cl-loop for comment in comments
                                                              collect (let* ((path (plist-get comment :path))
                                                                             (line (plist-get comment :line))
                                                                             (side (plist-get comment :side))
                                                                             (startside (plist-get comment :startside))
                                                                             (startline (plist-get comment :startline))
                                                                             (text (plist-get comment :body)))
                                                                        (append (list)
                                                                                (and path (list "-F" (concat "comments[][path]=" path)))
                                                                                (and startline (list "-F" (concat "comments[][start_line]=" (number-to-string startline))))
                                                                                (and startside (list "-F" (concat "comments[][start_side]=" startside)))
                                                                                (and line (list "-F" (concat "comments[][line]=" (number-to-string line))))
                                                                                (and side (list "-F" (concat "comments[][side]=" side)))
                                                                                (and text (list "-F" (concat "comments[][body]=" text))))))))))

    (let ((response (apply #'consult-gh--call-process "api" args)))
      (cond
       ((= (car response) 0)
        (let* ((table (consult-gh--json-to-hashtable (cadr response)))
               (id (and (hash-table-p table) (gethash :id table))))
          (prog2
              (message (concat (if event "Review " "Draft Pending Review ") "Submitted!"))
              (or id nil)
            (funcall consult-gh-quit-window-func t))))
       ((= (car response) 422)
        (let* ((table (consult-gh--json-to-hashtable (cadr response)))
               (id (and (hash-table-p table) (gethash :id table))))
          (prog2
              (message (cadr response))
              (or id nil)
            nil)))
       (t
        (message (cadr response))
        nil)))))

(defun consult-gh-topics--pr-review-presubmit (review)
  "Prepare pull request REVIEW to submit.

REVIEW is a string with properties that identify a github pull request
review.  For an example, see buffer-local variable `consult-gh--topic' in
the buffer generated by `consult-gh-pr-review'."
  (if consult-gh-topics-edit-mode
      (let* ((review (or review consult-gh--topic))
             (repo (get-text-property 0 :repo review))
             (number (get-text-property 0 :number review))
             (nextsteps (append (list (cons "Submit" :submit))
                                (list (cons "Add Comment on the Code" :code-comment))
                                (list (cons "Continue in the Browser" :browser))
                                (list (cons "Cancel" :cancel))))
             (next (consult--read nextsteps
                                  :prompt "Choose what to do next? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))
        (cond
         ((eq next :code-comment)
          (consult-gh-topics--pr-review-add-comment))
         ((eq next :submit)
          (let* ((body (consult-gh-topics--buffer-string))
                 (comments (consult-gh-topics--pr-review-get-comments))
                 (commit-id (plist-get (car (get-text-property 0 :comments review)) :commit-id))
                 (event (consult--read '(("Comment" . "COMMENT")
                                         ("Request Changes" . "REQUEST_CHANGES")
                                         ("Approve" . "APPROVE")
                                         ("Submit Review as Pending Draft" . nil))
                                       :prompt "Select Action: "
                                       :lookup #'consult--lookup-cdr
                                       :require-match t
                                       :sort nil)))
            (consult-gh-topics--pr-review-submit repo number body commit-id event comments)))
         ((eq next :browser)
          (let* ((body (consult-gh-topics--buffer-string))
                 (comments (get-text-property 0 :comments review))
                 (commit-id (plist-get (get-text-property 0 :comment-info review) :commit-id))
                 (event nil)
                 (id (consult-gh-topics--pr-review-submit repo number body commit-id event comments)))
            (when id
              (consult-gh--make-process "consult-gh-draft-review"
                                        :when-done (lambda (_ out)
                                                     (let* ((table (consult-gh--json-to-hashtable out))
                                                            (url (and (hash-table-p table) (gethash :html_url table))))
                                                       (when url
                                                         (funcall (or consult-gh-browse-url-func #'browse-url) url))))
                                        :cmd-args (list "api" "-H" "Accept: application/vnd.github.json" (format "repos/%s/pulls/%s/reviews/%s" repo number id))))))))
    (message "%s in a %s buffer!" (propertize "Not" 'face 'consult-gh-error) (propertize "pull request editing" 'face 'consult-gh-error))))

(defun consult-gh--search-code-format (string input highlight)
  "Format minibuffer candidates for code.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh search code ...”\).
  INPUT     the query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
            with `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "code")
         (type "code")
         (parts (string-split string ":"))
         (repo (car parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (path (format "%s" (cadr parts)))
         (api-url (format "repos/%s/contents/%s" repo path))
         (ref "HEAD")
         (path (concat "./" path))
         (code (mapcar (lambda (x) (replace-regexp-in-string "\t" "\s\s" (replace-regexp-in-string "\n" "\\n" (format "%s" x)))) (cdr (cdr parts))))
         (code (string-join code ":"))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\t%s\t%s"
                      (consult-gh--set-string-width (propertize code 'face  'consult-gh-code) 100)
                      (propertize path 'face 'consult-gh-url)
                      (consult-gh--set-string-width (concat (propertize user 'face 'consult-gh-user ) "/" (propertize package 'face 'consult-gh-package)) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :package package :code code :path path :api-url api-url :query query :class class :type type :ref ref) str)
    str))

(defun consult-gh--code-state ()
  "State function for code candidates.

This is passed as STATE to `consult--read' in `consult-gh-search-code'
and is used to preview or do other actions on the code."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (if cand
          (pcase action
            ('preview
             (if (and consult-gh-show-preview cand)
                 (let* ((repo (get-text-property 0 :repo cand))
                        (path (get-text-property 0 :path cand))
                        (ref (or (get-text-property 0 :ref cand) "HEAD"))
                        (code (get-text-property 0 :code cand))
                        (api-url (get-text-property 0 :api-url cand))
                        (tempdir (expand-file-name (concat repo "/" ref "/")
                                                   (or consult-gh--current-tempdir (consult-gh--tempdir))))
                        (temp-file (or (cdr (assoc (substring-no-properties (concat repo "/" "path")) consult-gh--open-files-list)) (expand-file-name path tempdir)))
                        (text (if api-url (consult-gh--files-get-content-by-api-url api-url)
                                (consult-gh--files-get-content-by-path repo path ref)))
                        (_ (progn
                             (unless (file-exists-p temp-file)
                               (make-directory (file-name-directory temp-file) t)
                               (with-temp-file temp-file
                                 (insert text)
                                 (set-buffer-file-coding-system 'raw-text)
                                 (set-buffer-multibyte t)
                                 (let ((after-save-hook nil))
                                   (write-file temp-file))
                                 (add-to-list 'consult-gh--open-files-list `(,(substring-no-properties (concat repo "/" path)) . ,temp-file))))))
                        (buffer (or (find-file-noselect temp-file t) nil)))
                   (when buffer
                     (with-current-buffer buffer
                       (if consult-gh-highlight-matches
                           (highlight-regexp (string-trim code) 'consult-gh-preview-match))
                       (goto-char (point-min))
                       (search-forward code nil t)
                       (add-to-list 'consult-gh--preview-buffers-list buffer)
                       (add-to-list 'consult-gh--open-files-buffers buffer)
                       (funcall preview action
                                buffer)
                       (consult-gh-recenter 'middle))))))
            ('return
             cand))))))

(defun consult-gh--code-group (cand transform)
  "Group function for code candidates, CAND.

This is passed as GROUP to `consult--read' in `consult-gh-search-code'
and is used to group code results.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-code-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Code " 98 nil ?-)
       (consult-gh--set-string-width " Path " 8 nil ?-)
       (consult-gh--set-string-width " > Repo " 40 nil ?-))))))

(defun consult-gh--code-browse-url-action (cand)
  "Browse the url for a code candidate, CAND.

This is an internal action function that gets a candidate, CAND,
from `consult-gh-search-code' and opens the url of the file
containing the code in an external browser.

To use this as the default action for code,
set `consult-gh-code-action' to `consult-gh--code-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (path (substring-no-properties (get-text-property 0 :path cand)))
         (url (concat (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")) "/blob/HEAD/" path)))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--code-view-action (cand)
  "Open code candidate, CAND.

This is a wrapper function around `consult-gh--files-view'.
It parses CAND to extract relevant values
\(e.g. repository, file path, ...\)
and passes them to `consult-gh--files-view'.

To use this as the default action on code candidates,
set `consult-gh-code-action' to `consult-gh--code-view-action'."
  (let* ((repo (get-text-property 0 :repo cand))
         (ref (or (get-text-property 0 :ref cand) "HEAD"))
         (code (get-text-property 0 :code cand))
         (tempdir (expand-file-name (concat repo "/" ref "/")
                                    (or consult-gh--current-tempdir (consult-gh--tempdir))))
         (path (get-text-property 0 :path cand))
         (api-url (get-text-property 0 :api-url cand)))
    (consult-gh--files-view repo path api-url nil tempdir code ref)))

(defun consult-gh--dashboard-format (string)
  "Format minibuffer candidates for dashboard items.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh search code ...”\)."
  (let* ((class "dashboard")
         (query "")
         (parts (string-split string "      "))
         (isPR (car parts))
         (type (if (equal isPR "true") "pr" "issue"))
         (repo (cadr parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (title (caddr parts))
         (title (and (stringp title)
                     (string-replace "\n" "  " title)))
         (number (cadddr parts))
         (state (cadddr (cdr parts)))
         (state (and (stringp state)
                     (upcase state)))
         (date (cadddr (cddr parts)))
         (date (and (stringp date) (substring date 0 10)))
         (tags (cadddr (cdddr parts)))
         (tags (and (stringp tags) (progn (string-match "\\[map\\[\\(.*\\)\\]" tags)
                                          (concat "[" (match-string 1 tags) "]"))))
         (url (cadddr (cdr (cdddr parts))))
         (commentscount (cadddr (cddr (cdddr parts))))
         (reason (cadddr (cdddr (cdddr parts))))
         (reason-str (and (stringp reason)
                          (cond
                           ((string-prefix-p "Assigned to" reason) "assigned")
                           ((string-prefix-p "Authored by" reason) "authored")
                           ((string-prefix-p "Mentions " reason) "mentions")
                           ((string-prefix-p "Involves " reason) "involves"))))
         (face (pcase isPR
                 ("false"
                  (pcase state
                    ("CLOSED" 'consult-gh-success)
                    ("OPEN" 'consult-gh-warning)
                    (_ 'consult-gh-issue)))
                 ("true"
                  (pcase state
                    ("CLOSED" 'consult-gh-error)
                    ("MERGED" 'consult-gh-success)
                    ("OPEN" 'consult-gh-warning)
                    (_ 'consult-gh-pr)))
                 (_ 'consult-gh-issue)))
         (str (concat (consult-gh--set-string-width
                       (concat (propertize (format "%s" user) 'face 'consult-gh-user)
                               "/"
                               (propertize (format "%s" package) 'face 'consult-gh-package)

                               (propertize (format " - %s #%s: " (upcase (substring type 0 2)) number) 'face face)
                               (propertize (format "%s" title) 'face 'consult-gh-default))
                       80)
                      (when reason-str (concat "\s\s" (propertize (consult-gh--set-string-width reason-str 8) 'face 'consult-gh-visibility)))
                      (when date (concat "\s\s" (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)))
                      (when state (concat "\s\s" (propertize (consult-gh--set-string-width state 6) 'face face)))
                      (when commentscount (concat "\s\s" (propertize (consult-gh--set-string-width commentscount 5) 'face ' consult-gh-visibility)))
                      (when tags (concat "\s\s" (propertize tags 'face 'consult-gh-tags))))))
    (add-text-properties 0 1 (list :repo repo
                                   :user user
                                   :package package
                                   :number number
                                   :comm commentscount
                                   :state state
                                   :title title
                                   :tags tags
                                   :date date
                                   :query query
                                   :type type
                                   :url url
                                   :reason reason
                                   :class class)
                         str)
    str))

(defun consult-gh--dashboard-state ()
  "State function for dashboard candidates.

This is passed as STATE to `consult--read' in `consult-gh-dashboard'
and is used to preview or do other actions on the code."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (type (get-text-property 0 :type cand))
                        (number (get-text-property 0 :number cand))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (pcase type
                 ("issue"
                  (consult-gh--issue-view (format "%s" repo) (format "%s" number) buffer)
                  (funcall preview action buffer))
                 ("pr"
                  (consult-gh--pr-view (format "%s" repo) (format "%s" number) buffer)
                  (funcall preview action buffer))
                 (_ (message "Preview is not supoorted for items that are not issues or pull requests!"))))))
        ('return
         cand)))))

(defun consult-gh--dashboard-group (cand transform)
  "Group function for dashboard candidates, CAND.

This is passed as GROUP to `consult--read' in `consult-gh-dashboard'
and is used to group items in the dashboard.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-dashboard-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Repo - Type Number: Title " 78 nil ?-)
       (consult-gh--set-string-width " Reason " 10 nil ?-)
       (consult-gh--set-string-width " Date " 12 nil ?-)
       (consult-gh--set-string-width " State " 8 nil ?-)
       (consult-gh--set-string-width " Comm " 7 nil ?-)
       " Tags ")))))

(defun consult-gh--dashboard-action (cand)
  "View dashboard item, CAND.

This is an internal action function that gets a dashboard candidate, CAND,
from `consult-gh-dashboard' and passes it to default actions for issues or
prs, discussions, etc.

To use this as the default action for issues,
set `consult-gh-dashboard-action' to `consult-gh--dashboard-action'."

  (let* ((type (get-text-property 0 :type cand))
         (url (get-text-property 0 :url cand)))
    (cond
     ((equal type "issue")
      (funcall consult-gh-issue-action cand))
     ((equal type "pr")
      (funcall consult-gh-pr-action cand))
     (url
      (funcall (or consult-gh-browse-url-func #'browse-url) url))
     (t
      (message "cannot open that with `consult-gh--dashboard-action'!")))))

(defun consult-gh--dashboard-browse-url-action (cand)
  "Browse the url for a dashboard candidate, CAND.

This is an internal action function that gets a dashboard candidate, CAND,
from `consult-gh-dashboard' and opens the url of the issue/pr
in an external browser.

To use this as the default action for issues,
set `consult-gh-dashboard-action' to `consult-gh--dashboard-browse-url-action'."
  (let* ((type (get-text-property 0 :type cand))
         (url (substring-no-properties (get-text-property 0 :url cand))))
    (if url
        (funcall (or consult-gh-browse-url-func #'browse-url) url)
      (cond
       ((equal type "issue")
        (funcall consult-gh-issue-action cand))
       ((equal type "pr")
        (funcall consult-gh-pr-action cand))))))

(defun consult-gh-notifications-make-args ()
  "Make cmd arguments for notifications."
  (list "api" (concat "notifications?sort=updated" (if consult-gh-notifications-show-unread-only "" "&all=true")) "--paginate" "--template" (concat "{{range .}}" "{{.id}}" "\t" "{{.subject.type}}" "\t" "{{.repository.full_name}}" "\t" "{{.subject.title}}" "\t" "{{.reason}}" "\t" "{{.unread}}" "\t" "{{.updated_at}}" "\t" "{{(timeago .updated_at)}}" "\t" "{{.subject.url}}" "      " "{{end}}")))

(defun consult-gh--notifications-format (string)
  "Format minibuffer candidates for notifications.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh search code ...”\)."
  (let* ((class "notification")
         (query "")
         (parts (string-split string "\t"))
         (thread (car parts))
         (type (cadr parts))
         (type (and (stringp type) (cond
                                    ((equal (downcase type) "pullrequest") "pr")
                                    ((equal (downcase type) "issue") "issue")
                                    (t (downcase type)))))
         (repo (caddr parts))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (title (cadddr parts))
         (reason (cadddr (cdr parts)))
         (unread (cadddr (cddr parts)))
         (face (pcase unread
                 ("true" 'consult-gh-default)
                 ("false" 'consult-gh-tags)
                 (_ 'consult-gh-default)))
         (state (pcase unread
                  ("true" "Unread")
                  ("false" "Seen")
                  (_ "Unknown")))
         (date (substring (cadddr (cdddr parts)) 0 10))
         (reltime (cadddr (cdr (cdddr parts))))
         (api-url (cadddr (cddr (cdddr parts))))
         (url-parts (and (stringp api-url) (split-string api-url "/" t)))
         (number (and url-parts
                      (or
                       (cadr (member "issues" url-parts))
                       (cadr (member "pulls" url-parts)))))
         (title-str (concat (propertize (format "%s" repo) 'face 'consult-gh-repo)
                            (propertize " - " 'face face)
                            (propertize (concat type (if number " #") number) 'face face)
                            (propertize ": " 'face face)
                            (propertize (format "%s" title) 'face face)))
         (_ (if (equal unread "false") (add-face-text-property 0 (length title-str) '(:strike-through t) t title-str)))
         (str (format "%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width title-str 80)
                      (propertize (consult-gh--set-string-width reason 13) 'face 'consult-gh-visibility)
                      (consult-gh--set-string-width (propertize state 'face face) 7)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date))))
    (add-text-properties 0 1 (list :thread thread
                                   :repo repo
                                   :user user
                                   :package package
                                   :number number
                                   :reason reason
                                   :state state
                                   :title title
                                   :date date
                                   :reltime reltime
                                   :query query
                                   :type type
                                   :api-url api-url
                                   :reason reason
                                   :class class)
                         str)

    str))

(defun consult-gh--notifications-state ()
  "State function for code candidates.

This is passed as STATE to `consult--read' in `consult-gh-notifications'
and is used to preview or do other actions on the code."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (let* ((repo (get-text-property 0 :repo cand))
                    (type (get-text-property 0 :type cand))
                    (number (get-text-property 0 :number cand))
                    (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (pcase type
                 ("issue" (consult-gh--issue-view (format "%s" repo) (format "%s" number) buffer))
                 ("pr" (consult-gh--pr-view (format "%s" repo) (format "%s" number) buffer))
                 (_ (message "Preview not supported for %s items" type)))
               (when (member type '("issue" "pr")) (funcall preview action
                                                            buffer)))))
        ('return
         cand)))))

(defun consult-gh--notifications-group (cand transform)
  "Group function for notifications candidates, CAND.

This is passed as GROUP to `consult--read' in `consult-gh-notifications'
and is used to group items in the natofications.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-notifications-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Repo - Type Number: Title " 78 nil ?-)
       (consult-gh--set-string-width " Reason " 15 nil ?-)
       (consult-gh--set-string-width " State " 9 nil ?-)
       (consult-gh--set-string-width " Date " 11 nil ?-))))))

(defun consult-gh--discussion-browse-url (repo title &optional date)
  "Browse the url for a discussion in REPO with TITLE.

Optional argument DATE is latest updated date of discussion."

  (let* ((filter (concat (format "filter=%s in:title repo:%s" title repo)
                         (when date (format " updated:>=%s" date))))
         (query "query=query($filter: String!) {search(query: $filter, type: DISCUSSION, first: 1) { nodes { ... on Discussion { number }}}}")
         (id (consult-gh--command-to-string "api" "graphql" "--paginate" "--cache" "24h" "-F" filter "-f" query "--template" "{{(index .data.search.nodes 0).number}}"))
         (url (concat "https://" (consult-gh--auth-account-host) (format "/%s/discussions/%s" repo id))))
    (when url (funcall (or consult-gh-browse-url-func #'browse-url) url))))

(defun consult-gh--discussion-browse-url-action (cand)
  "Open the discussion of CAND thread in the browser CAND.

This is a wrapper function around `consult-gh--discussion-browse-url'.
It parses CAND to extract relevant values \(e.g. repository's name and
discussion title\) and passes them to `consult-gh--discussion-browse-url'.

To use this as the default action for discussions,
set `consult-gh-discussion-action' to
`consult-gh--discussion-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (title (substring-no-properties (format "%s" (get-text-property 0 :title cand))))
         (date (substring-no-properties (get-text-property 0 :date cand))))
    (consult-gh--discussion-browse-url repo title date)))

(defun consult-gh--notifications-action (cand)
  "View notification item, CAND.

This is an internal action function that gets a notification candidate, CAND,
from `consult-gh-notifications' and passes it to default actions for issues or
prs, discussions, etc.

To use this as the default action for issues,
set `consult-gh-notifications-action' to `consult-gh--notifications-action'."

  (let* ((repo (get-text-property 0 :repo cand))
         (type (get-text-property 0 :type cand))
         (url (concat "https://" (consult-gh--auth-account-host) (format "/notifications?query=repo:%s" repo))))
    (pcase type
      ("issue"
       (funcall consult-gh-issue-action cand))
      ((or "pr" "pullrequest")
       (funcall consult-gh-pr-action cand))
      ("discussion"
       (funcall consult-gh-discussion-action cand))
      (_
       (and url (funcall (or consult-gh-browse-url-func #'browse-url) url))))
    t))

(defun consult-gh--notifications-browse-url-action (cand)
  "Browse the url for a notification candidate, CAND.

This is an internal action function that gets a notification candidate,
CAND, from `consult-gh-notifications' and opens the url with relevant
notifications in an external browser.

To use this as the default action for issues,
set `consult-gh-notificatios-action' to
`consult-gh--notifications-browse-url-action'."
  (if-let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
            (url (concat "https://" (consult-gh--auth-account-host) (format "/notifications?query=repo:%s" repo))))
      (funcall (or consult-gh-browse-url-func #'browse-url) url)
    (message "Cannot find the right url to open!")))

(defun consult-gh--notifications-mark-as-read (cand)
  "Mark CAND as read.

This is an internal action function that gets a notification candidate, CAND,
from `consult-gh-notifications' and marks it as read."
  (when-let* ((thread (get-text-property 0 :thread cand)))
    (when (consult-gh--command-to-string "api" (format "notifications/threads/%s" thread) "--silent" "--method" "PATCH")
      (message "marked as read!"))))

(defun consult-gh--notifications-unsubscribe (cand)
  "Unsubscribe from the thread of CAND.

This is an internal action function that gets a notification candidate, CAND,
from `consult-gh-notifications' and unsubscribes from the thread."
  (when-let* ((thread (get-text-property 0 :thread cand)))
    (when (consult-gh--command-to-string "api" "--method" "PUT" "-H" "Accept: application/vnd.github+json" "--paginate" "-F" "ignored=true"(format "/notifications/threads/%s/subscription" thread))
      (message "Unsubscribed!"))))

(defun consult-gh--notifications-subscribe (cand)
  "Unsubscribe from the thread of CAND.

This is an internal action function that gets a notification candidate, CAND,
from `consult-gh-notifications' and unsubscribes from the thread."
  (when-let* ((thread (get-text-property 0 :thread cand)))
    (when (consult-gh--command-to-string "api" "--method" "PUT" "-H" "Accept: application/vnd.github+json" "--paginate" "-F" "ignored=false" (format "/notifications/threads/%s/subscription" thread))
      (message "Subscribed!"))))

(defun consult-gh--release-format (string input highlight)
  "Format minibuffer candidates for releases.

Description of Arguments:

  STRING    the output of a “gh” call
            \(e.g. “gh release list ...”\).
  INPUT     the query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted
            with `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "release")
         (type "release")
         (parts (string-split string "\t"))
         (repo (car (consult--command-split input)))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (title (car parts))
         (state (cadr parts))
         (draft (equal state "Draft"))
         (latest (equal state "Latest"))
         (prerelease (equal state "Pre-release"))
         (tagname (cadr (cdr parts)))
         (date (cadr (cddr parts)))
         (date (if (and (stringp date) (length> date 9)) (substring date 0 10) date))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                      (consult-gh--set-string-width title 35)
                      (propertize (consult-gh--set-string-width tagname 20) 'face 'consult-gh-issue)
                      (propertize (consult-gh--set-string-width date 10) 'face 'consult-gh-date)
                      (consult-gh--set-string-width state 15)
                      (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user)) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :package package :tagname tagname :title title :state state :draft draft :latest latest :prerelease prerelease :date date :query query :class class :type type) str)
    str))

(defun consult-gh--release-state ()
  "State function for release candidates.

This is passed as STATE to `consult--read' in `consult-gh-release-list'
and is used to preview or do other actions on the issue."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (query (get-text-property 0 :query cand))
                        (tagname (get-text-property 0 :tagname cand))
                        (match-str (consult--build-args query))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (consult-gh--release-view (format "%s" repo) (format "%s" tagname) buffer)
               (with-current-buffer buffer
                 (if consult-gh-highlight-matches
                     (cond
                      ((listp match-str)
                       (mapc (lambda (item)
                                 (highlight-regexp item 'consult-gh-preview-match))
                             match-str))
                      ((stringp match-str)
                       (highlight-regexp match-str 'consult-gh-preview-match)))))
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun consult-gh--release-group (cand transform)
  "Group function for release.

This is passed as GROUP to `consult--read' in
`consult-gh-release-list', and is used to group releases.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-releases-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Title " 33 nil ?-)
       (consult-gh--set-string-width " TagName " 22 nil ?-)
       (consult-gh--set-string-width " Date " 12 nil ?-)
       (consult-gh--set-string-width " State " 17 nil ?-)
       (consult-gh--set-string-width " Repo " 42 nil ?-))))))

(defun consult-gh--release-browse-url-action (cand)
  "Browse the url for a release candidate, CAND.

This is an internal action function that gets a candidate, CAND,
for example from `consult-gh-release-list' and opens the url of the
release in an external browser.

To use this as the default action for releases,
set `consult-gh-release-action' to
`consult-gh--release-browse-url-action'."
  (let* ((repo (get-text-property 0 :repo cand))
         (tagname (get-text-property 0 :tagname cand))
         (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")))
         (url (and repo-url (concat repo-url "/releases/" tagname))))
 (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--release-read-json (repo tagname &optional json)
  "Get json response of release with TAGNAME in REPO.

Runs an async shell command with the command:
gh release view TAGNAME --repo REPO --json JSON
and returns the output as a hash-table.

Optional argument JSON, defaults to `consult-gh--reelase-view-json-fields'."
  (let* ((json-object-type 'hash-table)
         (json-array-type 'list)
         (json-key-type 'keyword)
         (json-false :false))
    (json-read-from-string (consult-gh--command-to-string "release" "view" tagname "--repo" repo "--json" (or json consult-gh--release-view-json-fields)))))

(defun consult-gh--release-get-discussion (repo tagname)
  "Get discussion url of reelase with TAGNAME in REPO."
  ;; do not message if discussion url does not exist
  (let ((inhibit-message t))
    (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (format "repos/%s/releases/tags/%s" repo tagname)) :discussion_url)))

(defun consult-gh--release-format-header (repo table &optional topic)
  "Format a header for a release of TAGNAME in REPO.

TABLE is a hash-table output containing release information
from `consult-gh--release-read-json'.  Returns a formatted string containing
the header section for `consult-gh--release-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--release-view'."
  (let* ((title (gethash :name table))
         (author (gethash :login (gethash :author table)))
         (author (and (stringp author)
                                     (propertize author 'help-echo (apply-partially #'consult-gh--get-user-tooltip author) 'rear-nonsticky t)))
         (tagname (gethash :tagName table))
         (draft (gethash :isDraft table))
         (draft (if (or (equal draft :false)
                                      (equal draft "false"))
                                  nil
                                draft))
         (prerelease (gethash :isPrerelease table))
         (prerelease (if (or (equal prerelease :false)
                                           (equal prerelease "false"))
                                       nil
                                     prerelease))
         (target (gethash :targetCommitish table))
         (tarballUrl (gethash :tarballUrl table))
         (zipballUrl (gethash :zipballUrl table))
         (uploadUrl (gethash :uploadUrl table))
         (createdAt (gethash :createdAt table))
         (publishedAt (gethash :publisheddAt table))
         (publishedAt (and publishedAt (format-time-string "[%Y-%m-%d %H:%M]" (date-to-time publishedAt))))
         (url (gethash :url table))
         (discussion (consult-gh--release-get-discussion repo tagname)))

    (when (stringp topic)
      (add-text-properties 0 1 (list :title title :tagname tagname :target target :author author :published publishedAt :created createdAt :draft draft :prerelease prerelease :discussion discussion :url url :uploadUrl uploadUrl :zipballUrl zipballUrl :tarballUrl tarballUrl) topic))

    (concat "title: " title "\n"
            "tag: " tagname "\n"
            (if draft "draft: true\n" "draft: false\n")
            (if prerelease "prerelease: true\n" "prerelease: false\n")
            "author: " author "\n"
            "repository: " (propertize repo 'help-echo (apply-partially #'consult-gh--get-repo-tooltip repo)) "\n"

            (and createdAt (concat "created: " createdAt "\n"))
            (and publishedAt (concat "published: " publishedAt "\n"))
            (and url (concat "url: " url "\n"))
            (and discussion (concat "discussion_url: " discussion "\n"))
            "\n--\n")))

(defun consult-gh--release-format-body (table &optional topic)
  "Format a body section for a release stored in TABLE.

This function returns a formatted string containing the body section for
`consult-gh--release-view'.

TABLE is a hash-table output from `consult-gh--release-read-json'
containing release's body under the key :body.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--release-view'."
  (let* ((body (gethash :body table)))
    (when topic (add-text-properties 0 1 (list :body body) topic))
    body))

(defun consult-gh--release-view (repo tagname &optional buffer preview)
  "Open release with TAGNAME in REPO in BUFFER.

This is an internal function that takes REPO, the full name of
a GitHub repository \(e.g. “armindarvish/consult-gh”\) and a TAGNAME,
th tag for a release, and shows the release description in an Emacs
buffer.

It fetches the preview from GitHub by
“gh release view TAGNAME --repo REPO” using `consult-gh--call-process'
and put it as raw text in either BUFFER or if BUFFER is nil,
in a buffer named by `consult-gh-preview-buffer-name'.
If `consult-gh-release-preview-major-mode' is non-nil, uses it as
major-mode, otherwise shows the raw text in \='fundamental-mode.

Description of Arguments:
  REPO     a string; the full name of the repository
  TAGNAME  a string; tagname of a release
  BUFFER   a string; optional buffer name
  PREVIEW  a boolean; whether to load the preview without details."
(consult--with-increased-gc
    (let* ((topic (format "%s/%s" repo tagname))
           (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
           (table (consult-gh--release-read-json repo tagname))
           (header-text (consult-gh--release-format-header repo table topic))
           (body-text (consult-gh--release-format-body table topic)))

      (add-text-properties 0 1 (list :repo repo :type "release") topic)

      (unless preview
      (consult-gh--completion-set-all-fields repo topic (consult-gh--user-canwrite repo)))

 (with-current-buffer buffer
       (let ((inhibit-read-only t))
         (erase-buffer)
         (fundamental-mode)
         (when header-text (insert header-text))
         (save-excursion
           (when (eq consult-gh-release-preview-major-mode 'org-mode)
             (consult-gh--github-header-to-org buffer)))
         (when body-text (insert body-text))
         (consult-gh--format-view-buffer "release")
         (consult-gh-release-view-mode +1)
         (setq-local consult-gh--topic topic)
         (current-buffer))))))

(defun consult-gh--release-view-action (cand)
  "Open the preview of a release candidate, CAND.

This is a wrapper function around `consult-gh--release-view'.
It parses CAND to extract relevant values \(e.g. releases's tag name\) and
passes them to `consult-gh--release-view'.

To use this as the default action for releases, set
`consult-gh-release-action' to function `consult-gh--release-view-action'."

  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (tagname (substring-no-properties (format "%s" (get-text-property 0 :tagname cand))))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/releases/" tagname "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the release in the existing buffer." :replace)
                             (cons "Make a new buffer and load the release in it (without killing the old buffer)." :new))
                       :prompt "You already have this release open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))
    (if existing
        (cond
         ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
         ((eq confirm :replace)
          (message "Reloading release in the existing buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--release-view repo tagname existing)))
         ((eq confirm :new)
          (message "Opening release in a new buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--release-view repo tagname (generate-new-buffer buffername nil)))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--release-view repo tagname))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--release-delete (repo tagname &optional noconfirm)
"Delete release with TAGNAME in REPO.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-release-delete'.

It runs the command “gh release delete TAGNAME --repo REPO”
using `consult-gh--command-to-string'.

When NOCONFIRM is non-nil, does not ask for confirmation."
(let* ((confirm (or noconfirm
                    (y-or-n-p (format "Delete release %s in %s?" tagname repo))))
       (cleanup (y-or-n-p (format "Do you want to delete the tag for %s as well?" tagname)))
       (args (list tagname "--repo" repo)))
  (when cleanup
      (setq args (append args (list "--cleanup-tag"))))

  (and confirm
       (apply #'consult-gh--command-to-string "release" "delete" args)
       (message "release %s in %s was %s" (propertize tagname 'face 'consult-gh-warning) (propertize repo 'face 'consult-gh-repo) (propertize "DELETED!" 'face 'consult-gh-warning)))))

(defun consult-gh--release-delete-action (cand)
  "Delete a release candidate, CAND.

This is a wrapper function around `consult-gh--release-delete'.
It parses CAND to extract relevant values \(e.g. repository's name\)
and passes them to `consult-gh--release-delete'.

To use this as the default action for repos,
set `consult-gh-release-action' to `consult-gh--release-delete-action'.
If `consult-gh-confirm-before-delete-release' is non-nil it asks for
confirmation before deleting the release, otherise deletes the release
without asking for confirmation."

  (let* ((repo (get-text-property 0 :repo cand))
         (tagname (get-text-property 0 :tagname cand)))
    (if consult-gh-confirm-before-delete-release
        (consult-gh--release-delete repo tagname)
      (consult-gh--release-delete repo tagname t))))

(defun consult-gh--release-download (repo tagname &optional skip-existing)
  "Download assets of release TAGNAME in REPO.

This is an internal function for non-interactive use.
For interactive use see `consult-gh-release-download`.

It runs the command “gh release download TAGNAME --repo REPO”
using `consult-gh--command-to-string'.

When SKIP-EXISTING is non-nil, does not overwrite existing files"
  (let* ((nextsteps (append (list (cons "Download all the assets from the release" :download))
                            (list (cons "Download the archive of the source code for release" :archive))
                            (list (cons "Download files with specific patterns (e.g. '*.deb') from the assets" :pattern))
                            (list (cons "Cancel" :cancel))))
         (next (when nextsteps (consult--read nextsteps
                                              :prompt "Choose What do you want to download? "
                                              :lookup #'consult--lookup-cdr
                                              :sort nil)))
         (args (append
                (list "release" "download" tagname "--repo" (substring-no-properties repo))
                (if skip-existing
                    (list "--skip-existing")
                  (list "--clobber")))))

    (when (and repo tagname next (not (equal next :cancel)))
      (pcase next
        (':download)
        (':archive (let ((archive (consult--read (list "zip" "tar.gz")
                                                 :prompt "Select the format for the archive: "
                                                 :require-match t)))
                     (setq args (append args (list (format "--archive=%s" archive))))))

        (':pattern (let ((patterns (consult--read nil
                                                  :prompt "Enter comma separated patterns (e.g. *.deb, *.rpm): ")))
                     (when (and patterns
                                (stringp patterns)
                                (not (string-empty-p patterns)))
                       (mapc (lambda (item) (setq args (append args (list "-p" item)))) (split-string patterns "," t "[\s\t\n\r]+"))))))

      (let ((dir (read-directory-name (concat "Select Directory for " (propertize (format "%s - %s " repo tagname) 'face 'font-lock-keyword-face)) (or (and (stringp consult-gh-default-save-directory) (file-name-as-directory consult-gh-default-save-directory)) default-directory))))

        (unless (file-exists-p dir)
          (make-directory (file-truename dir) t))

        (setq args (append args (if dir (list "--dir" (file-truename dir)))))
        (consult-gh--make-process (format "consult-gh-release-download-all-%s-%s" repo tagname)
                                  :when-done (lambda (_ str) (message str))
                                  :cmd-args args)))))

(defun consult-gh--release-download-action (cand)
  "Download assets of a release candidate, CAND.

This is a wrapper function around `consult-gh--release-download'.
It parses CAND to extract relevant values \(e.g. repository's name\ and
tag name) and passes them to `consult-gh--release-download'.

To use this as the default action for releases,
set `consult-gh-release-action' to `consult-gh--release-download-action'."
  (let* ((repo (get-text-property 0 :repo cand))
         (tagname (get-text-property 0 :tagname cand)))
         (consult-gh--release-download repo tagname)))

(defun consult-gh--release-generate-notes (repo &optional tagname previous-tag target topic)
  "Generate release notes for TAG in REPO.

Description of Arguments:
  REPO         a string; full name of repository
  TAGNAME      a string; tag name for release
  PREVIOUS-TAG a string; previous tag to generate notes from
  TARGET       a string; target branch/reference for release
  TOPIC        a string; string with properties that identify the topic (see
               `consult-gh--topic' for example)."
  (let* ((topic (or topic consult-gh--topic))
         (json (consult-gh--api-get-json (format "/repos/%s/releases/latest" repo)))
         (latest (if (= (car json) 0)
                     (consult-gh--json-to-hashtable (cdr json) :tag_name)))
         (tags (or (get-text-property 0 :valid-release-tags topic)
                   (and (not (member :valid-release-tags (text-properties-at 0 topic)))
                        (consult-gh--repo-get-tags repo t))))
         (tagname (or tagname (and (listp tags)
                           (consult--read tags
                                          :prompt "Select/Create a Tag: "))))
         (tagname (and (stringp tagname)
                       (not (string-empty-p tagname))
                       tagname))
         (target (or target (consult-gh--read-branch repo nil "Select Target Branch: " t nil)))
         (target (and (stringp target)
                      (not (string-empty-p target))
                      (substring-no-properties target)))
         (previous-tag (apply #'consult--read tags
                              (append (list :prompt "Select the Previous Tag: ")
                                      (if (and previous-tag (member previous-tag tags))
                                               (list :default previous-tag)))))
         (previous-tag (and (stringp previous-tag)
                            (not (string-empty-p previous-tag))
                            (member previous-tag tags)
                            previous-tag))
         (args (list "-X" "POST" "-H" "Accept: application/vnd.github+json")))
    (cond
     (tagname
        (setq args (append args
                           (list (format "/repos/%s/releases/generate-notes" repo))
                           (list "-f" (format "tag_name=%s" tagname))
                           (and target (list "-f" (format "target_commitish=%s" target)))
                           (and previous-tag (list "-f" (format "previous_tag_name=%s" previous-tag)))))

      (add-text-properties 0 1 (list :release-notes (list :previous-tag (or previous-tag latest) :tagname tagname :target target)) topic)
      (let* ((response (apply #'consult-gh--call-process "api" args)))
        (cond
         ((= (car response) 0)
          (let* ((table (consult-gh--json-to-hashtable (cadr response))))
            (when (hash-table-p table)
              (gethash :body table))))
         (t
          (message (cadr response))
          nil))))
     (t
      (progn (message "Require a tag name to generate notes!")
             nil)))))

(defun consult-gh--release-match-tag-and-target (release tagname target)
"Check if commit from existing TAGNAME matches TARGET in RELEASE."
  (let* ((repo (get-text-property 0 :valid-release-tags release))
         (tags (or (get-text-property 0 :valid-release-tags release)
                   (and (not (member :valid-release-tags (text-properties-at 0 release)))
                              (consult-gh--repo-get-tags repo t))))
         (targets (or (get-text-property 0 :valid-refs release)
                   (and (not (member :valid-refs (text-properties-at 0 release)))
                        (consult-gh--repo-get-branches-list repo t))))
         (done nil))

    (while (and (not done) (member tagname tags) target)
      (let ((commit-from-tag (get-text-property 0 :sha (car (member tagname tags))))
            (commit-from-target (if  (member target targets)
                                    (get-text-property 0 :sha (car (member target targets)))
                                  target)))
        (if (equal commit-from-tag commit-from-target)
            (setq done t)
        (pcase (consult--read (list (cons "Create a New Tag Name" :new-tag)
                                   (cons "Discard the target and use the commit from the existing tag" :existing))
           :prompt "That Tag Name already exists and is pinned to a different target.  What do you want to do?"
           :require-match t
           :lookup #'consult--lookup-cdr)
            (':existing (setq target nil))
            (':new-tag (let ((new-tag (consult--read nil
                                                     :prompt (format "New Tag Name (chage form %s): " tagname))))
                         (setq tagname new-tag)))))))
    (cons tagname target)))

(defun consult-gh-topics--release-parse-metadata ()
  "Parse release topic metadata."
  (let* ((region (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (header (when region (buffer-substring-no-properties (car region) (cdr region))))
         (tagname (when (and header (string-match ".*\\(?:\n.*tag:\\)\\(?1:.*\\)\n" header))
                      (match-string 1 header)))
         (target (when (and header (string-match ".*\\(?:\n.*target:\\)\\(?1:.*?\\)\n" header))
                      (match-string 1 header)))
         (draft (when (and header (string-match ".*\\(?:\n.*draft:\\)\\(?1:.*?\\)\n" header))
                      (match-string 1 header)))
         (prerelease (when (and header (string-match ".*\\(?:\n.*prerelease:\\)\\(?1:.*?\\)\n" header))
                      (match-string 1 header))))
  (list (and (stringp tagname)
             (string-trim tagname))
        (and (stringp target)
             (string-trim target))
        (and (stringp draft)
             (string-trim draft))
        (and (stringp prerelease)
             (string-trim prerelease)))))

(defun consult-gh-topics--release-get-metadata (&optional release)
  "Get metadata of RELEASE.

REALESE defaults to `consult-gh--topic'."

  (let* ((release (or release consult-gh--topic))
         (repo (get-text-property 0 :repo release))
         (canwrite (consult-gh--user-canwrite repo))
         (tagname (get-text-property 0 :tagname release))
         (target (get-text-property 0 :target release))
         (draft (get-text-property 0 :draft release))
         (prerelease (get-text-property 0 :prerelease release)))

    (when canwrite
      (pcase-let* ((`(,text-tag ,text-target ,text-draft ,text-prerelease) (consult-gh-topics--release-parse-metadata)))

        (when (and (stringp text-target) (not (string-empty-p text-target)))
          (cond
           ((member text-target (or (get-text-property 0 :valid-refs release) (consult-gh--repo-get-branches-list repo t)))
            (setq target text-target))
           (t (message "target not valid!")
              (setq target nil))))

        (when (and (stringp text-tag) (not (string-empty-p text-tag)))
          (setq tagname (string-replace " " "-" text-tag)))

        (when (and (stringp text-draft) (not (string-empty-p text-draft)))
          (setq draft (cond
                       ((equal (downcase text-draft) "true")
                          t)
                       ((equal (downcase text-draft) "false")
                        nil)
                       (t :invalid))))

        (when (and (stringp text-prerelease) (not (string-empty-p text-prerelease)))
          (setq prerelease (cond
                       ((equal (downcase text-prerelease) "true")
                          t)
                       ((equal (downcase text-prerelease) "false")
                        nil)
                       (t :invalid))))

        (when (and (stringp text-tag) (not (string-empty-p text-tag)))
          (setq tagname (string-replace " " "-" text-tag)))))

    (list (cons "tagname" tagname)
          (cons "target" target)
          (cons "draft" draft)
          (cons "prerelease" prerelease))))

(defun consult-gh-topics--release-create-change-tag (&optional repo release)
  "Change tag of RELEASE topic for REPO.

This is used for creating new releases."
  (pcase-let* ((release (or release consult-gh--topic))
         (meta (consult-gh-topics--release-get-metadata release))
         (repo (or repo (get-text-property 0 :repo release)))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (target (cdr (assoc "target" meta)))
         (current (cdr (assoc "tag" meta)))
         (tags (or (get-text-property 0 :valid-release-tags release)
                         (and (not (member :valid-release-tags (text-properties-at 0 release)))
                              (consult-gh--repo-get-tags repo t))))
         (selection (and (listp tags)
                         (consult--read tags
                                        :prompt "Select/Create a Tag: "
                                        :default current)))
         (`(,selection . ,new-target) (consult-gh--release-match-tag-and-target release selection target)))
    (if (string-empty-p selection) (setq selection nil))

    (unless (equal new-target target)
           (add-text-properties 0 1 (list :target new-target) release)
           (save-excursion (goto-char (car header))
                             (when (re-search-forward "^.*target: \\(?1:.*\\)?" (cdr header) t)
                                 (replace-match (or (get-text-property 0 :target release) "") nil nil nil 1))))

    (add-text-properties 0 1 (list :tagname selection) release)

    (save-excursion (goto-char (car header))
                    (when (re-search-forward "^.*tag: \\(?1:.*\\)?" (cdr header) t)
                      (replace-match (or (get-text-property 0 :tagname release) "") nil nil nil 1)))))

(defun consult-gh-topics--release-create-change-target (&optional repo release)
  "Change target of RELEASE topic for REPO.

This is used for creating new releases."
  (pcase-let* ((release (or release consult-gh--topic))
         (meta (consult-gh-topics--release-get-metadata release))
         (repo (or repo (get-text-property 0 :repo release)))
         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
         (tagname (cdr (assoc "tagname" meta)))
         (current (cdr (assoc "target" meta)))
         (targets (or (get-text-property 0 :valid-refs release)
                      (and (not (member :valid-refs (text-properties-at 0 release)))
                       (consult-gh--repo-get-branches-list repo t))))
         (selection (and (listp targets)
                         (consult--read targets
                                        :prompt "Select a Target: "
                                        :default current)))
         (`(,new-tagname . ,selection) (consult-gh--release-match-tag-and-target release tagname selection)))
    (if (string-empty-p selection) (setq selection nil))

    (unless (equal new-tagname tagname)
           (add-text-properties 0 1 (list :tagname new-tagname) release)
           (save-excursion (goto-char (car header))
                             (when (re-search-forward "^.*tag: \\(?1:.*\\)?" (cdr header) t)
                                (replace-match (or (get-text-property 0 :tagname release) "") nil nil nil 1))))

    (add-text-properties 0 1 (list :target selection) release)


             (save-excursion (goto-char (car header))
                             (when (re-search-forward "^.*target: \\(?1:.*\\)?" (cdr header) t)
                                 (replace-match (or (get-text-property 0 :target release) "") nil nil nil 1)))))

(defun consult-gh-topics--release-create-submit (repo tagname target title notes &optional notlatest prerelease discussion draft)
  "Create a new release in REPO with metadata.

Description of Arguments:
  TAGNAME    a string; full name of the base (target) repository
  TARGET     a string; name of the base ref branch
  TITLE      a string; title of the pr
  NOTES      a string; body of the pr
  NOTLATEST  a boolean; if non-nil do not mark as latest
  PRERELEASE a boolean; whether to mark the release as prerelease
  DISCUSSION a string; if non-nil adda discussion undeer this category
             for release
  DRAFT      a boolean; whether to submit release as draft?"
  (let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t "Select the repo you want to make a release for: "))))
         (tags (if tagname
                   nil
                 (or (get-text-property 0 :valid-release-tags consult-gh--topic)
                     (and (not (member :valid-release-tags (text-properties-at 0 consult-gh--topic)))
                              (consult-gh--repo-get-tags repo t)))))
         (tagname (or tagname (consult--read tags
                                     :prompt "Select/Create a Tag: "
                                     :default tagname)))
         (title (or title (consult--read nil :prompt "Title: ")))
         (notes (or notes (consult--read nil :prompt "Release Notes: ")))
         (args nil))

    (if (and repo tagname)
        (progn
          (setq args (delq nil (append args
                                   (list tagname)
                                   (list "--repo" repo)
                                   (and title (list "--title" (substring-no-properties title)))
                                   (and notes (list "--notes"  (substring-no-properties notes)))
                                   (and target (list "--target" target))
                                   (and draft (list "--draft"))
                                   (and prerelease (list "--prerelease"))
                                   (and discussion (list "--discussion-category" discussion))
                                   (and notlatest (and (list "--latest=false"))))))
      (apply #'consult-gh--command-to-string "release" "create" args))
      (progn (message "Tag name is required for creating a release!")
             nil))))

(defun consult-gh-topics--release-create-presubmit (release)
  "Prepare RELEASE to submit for creating a new pull request.

RELEASE is a string with properties that identify a github release.
For an example, see  the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh-release-create'."
  (if consult-gh-topics-edit-mode
      (let* ((release (or release consult-gh--topic))
             (repo (get-text-property 0 :repo release))
             (canwrite (consult-gh--user-canwrite repo)))
        (if canwrite
            (let* ((nextsteps (append
                                (list (cons "Publish Release" :submit))
                                (list (cons "Save as Draft" :draft))
                                (list (cons "Continue in the Browser" :browser))
                                (list (cons "Change the Tag" :tag))
                                (list (cons "Change the Target" :target))
                                (list (cons "Cancel" :cancel))))
                   (next (consult--read nextsteps
                                  :prompt "Choose what to do next? "
                                  :lookup #'consult--lookup-cdr
                                  :sort nil)))

        (while (or (eq next ':tag) (eq next ':target))
          (cond
           ((eq next ':tag)
           (consult-gh-topics--release-create-change-tag))
           ((eq next ':target)
            (consult-gh-topics--release-create-change-target)))

          (setq next (consult--read nextsteps
                                    :prompt "Choose what to do next? "
                                    :lookup #'consult--lookup-cdr
                                    :sort nil)))

        (pcase-let* ((`(,title . ,body) (consult-gh-topics--get-title-and-body))
                     (title (or title
                                (and (derived-mode-p 'org-mode)
                                     (cadar (org-collect-keywords
                                             '("title"))))
                                ""))
                     (body (or body ""))
                     (metadata (consult-gh-topics--release-get-metadata))
                     (tagname (cdr (assoc "tagname" metadata)))
                     (target (cdr (assoc "target" metadata)))
                     (`(,tagname . ,target) (consult-gh--release-match-tag-and-target release tagname target)))

          (pcase next
            (':browser (let* ((url (consult-gh-topics--release-create-submit repo tagname target title body nil nil nil t))
                             (urlobj (and url (url-generic-parse-url url))))
                         (and (url-p urlobj)
                              (browse-url (string-trim url)))))
            (':submit
             (let* ((latest (y-or-n-p "Mark this as the latest release?"))
                    (prerelease (y-or-n-p "Is this a prerelease?"))
                    (discussion (and (consult-gh--repo-has-discussions-enabled-p repo)
                                     (y-or-n-p "Would you like to add a discussion for this release?")
                                     (consult--read (consult-gh--get-discussion-categories repo)
                                                    :prompt "Select a category: "
                                                    :require-match t
                                                    :sort nil)))
                    (discussion (and (stringp discussion)
                                     (not (string-empty-p discussion))
                                     discussion)))

               (and (consult-gh-topics--release-create-submit repo tagname target title body (not latest) prerelease discussion nil)
                           (message "Release Published!")
                           (funcall consult-gh-quit-window-func t))))
            (':draft (let* ((latest (y-or-n-p "Mark this as the latest release?"))
                            (prerelease (y-or-n-p "Is this a prerelease?")))
                       (and  (consult-gh-topics--release-create-submit repo tagname target title body (not latest) prerelease nil t)
                          (message "Draft Saved!")
                          (funcall consult-gh-quit-window-func t)))))))
          (message "Current user does not have permission to create a release in this repo.")))
    (message "Not in a release editing buffer!")))

(defun consult-gh-release--edit-restore-default (&optional release)
  "Restore default values when editing RELEASE."

  (if consult-gh-topics-edit-mode
      (let* ((release (or release consult-gh--topic))
             (type (get-text-property 0 :type release)))
        (if (equal type "release")
            (let* ((title (get-text-property 0 :original-title release))
                   (body (get-text-property 0 :original-body release))
                   (tagname (get-text-property 0 :original-tagname release))
                   (target (get-text-property 0 :original-target release))
                   (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
                   (header-beg (car-safe header))
                   (header-end (cdr-safe header)))

              (add-text-properties 0 1 (list :title title :tagname tagname :target target :body body) release)

              (save-excursion
                ;; change title
                (goto-char (point-min))
                (when (re-search-forward "^.*title: \\(?1:.*\\)?" nil t)
                  (replace-match (get-text-property 0 :title release) nil nil nil 1))

                ;; change tagname
                (goto-char (or header-beg (point-min)))
                (when (re-search-forward "^.*tag: \\(?1:.*\\)?" nil t)
                  (replace-match (get-text-property 0 :tagname release) nil nil nil 1))

                ;;change target
                (goto-char (or header-beg (point-min)))
                (when (re-search-forward "^.*target: \\(?1:.*\\)?" header-end t)
                  (replace-match (get-text-property 0 :target release) nil nil nil 1))

                ;; change body
                (goto-char (or header-end (point-max)))
                (delete-region (point) (point-max))
                (insert body)))
          (message "No release topic to edit!")))
    (error "Not in a release editing buffer!")))

(defun consult-gh-release--edit-change-tagname (&optional new old release)
  "Change tag name of RELEASE from OLD to NEW."
(if consult-gh-topics-edit-mode
  (let* ((release (or release consult-gh--topic))
         (old-target (get-text-property 0 :target release))
         (type (get-text-property 0 :type release)))
    (if (equal type "release")
        (pcase-let* ((repo (get-text-property 0 :repo release))
               (tags (or (get-text-property 0 :valid-release-tags release)
                         (and (not (member :valid-release-tags (text-properties-at 0 release)))
                              (consult-gh--repo-get-tags repo t))))
               (new (or new (consult--read tags
                                           :initial old
                                           :prompt "New Tag Name: ")))
               (`(,new . ,new-target) (consult-gh--release-match-tag-and-target release new old-target))
               (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
               (header-end (and header (cdr-safe header))))

          (unless (equal new-target old-target)
            (add-text-properties 0 1 (list :target new-target) release)
            (save-excursion (goto-char (point-min))
                            (when (re-search-forward "^.*target: \\(?1:.*\\)?" header-end t)
                              (replace-match (get-text-property 0 :target release) nil nil nil 1))))

          (cond
           ((or (not new) (and new (stringp new) (string-empty-p new)))
            (message "tag name cannot be empty!"))
           ((and new (stringp new) (not (string-empty-p new)))
            (add-text-properties 0 1 (list :tagname new) release)
            (save-excursion (goto-char (point-min))
                            (when (re-search-forward "^.*tag: \\(?1:.*\\)?" header-end t)
                              (replace-match (get-text-property 0 :tagname release) nil nil nil 1))))))
      (message "No release topic to edit!")))
  (message "Not in a release editing buffer!")))

(defun consult-gh-release--edit-change-title (&optional new old release)
  "Change title of RELEASE from OLD to NEW."
(if consult-gh-topics-edit-mode
  (let* ((release (or release consult-gh--topic))
         (type (get-text-property 0 :type release)))
    (if (equal type "release")
        (let* ((new (or new (consult--read nil
                                           :initial old
                                           :prompt "New Title: ")))
               (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
               (header-end (and header (cdr-safe header))))

          (add-text-properties 0 1 (list :title new) release)

          (when (stringp new)
            (save-excursion (goto-char (point-min))
                            (when (re-search-forward "^.*title: \\(?1:.*\\)?" header-end t)
                              (replace-match (get-text-property 0 :title release) nil nil nil 1)))))
      (message "No release topic to edit!")))
  (message "Not in a release editing buffer!")))

(defun consult-gh-release--edit-change-body (&optional new release)
  "Change body of RELEASE to NEW."
(if consult-gh-topics-edit-mode
  (let* ((release (or release consult-gh--topic))
         (type (get-text-property 0 :type release)))
    (if (equal type "release")
        (let* ((new (or new (consult--read nil
                                           :prompt "New Body: ")))
               (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
               (header-end (and header (cdr-safe header))))

     (when (and (stringp new) (not (string-empty-p new)))
          (add-text-properties 0 1 (list :body new) release)
          (save-excursion (goto-char (or header-end (point-max)))
                          (delete-region (point) (point-max))
                          (insert new))))
      (message "No release topic to edit!")))
  (message "Not in a release editing buffer!")))

(defun consult-gh-release--edit-change-target (&optional new old release)
  "Change target branch of RELEASE from OLD to NEW."
  (if consult-gh-topics-edit-mode
      (let* ((release (or release consult-gh--topic))
             (type (get-text-property 0 :type release)))
        (if (equal type "release")
            (pcase-let* ((old-tagname (get-text-property 0 :tagname release))
                         (repo (get-text-property 0 :repo release))
                         (targets (or (get-text-property 0 :valid-refs release)
                                      (and (not (member :valid-refs (text-properties-at 0 release)))
                                           (consult-gh--repo-get-branches-list repo t))))
                         (new (or new (consult--read targets
                                                     :initial old
                                                     :prompt "New Target: ")))
                         (`(,new-tagname . ,new) (consult-gh--release-match-tag-and-target release old-tagname new))
                         (header (car (consult-gh--get-region-with-overlay ':consult-gh-header)))
                         (header-end (and header (cdr-safe header))))

              (unless (equal new-tagname old-tagname)
                (add-text-properties 0 1 (list :tagname new-tagname) release)
                (when (stringp new-tagname)
                  (save-excursion (goto-char (point-min))
                                  (when (re-search-forward "^.*tag: \\(?1:.*\\)?" header-end t)
                                    (replace-match (get-text-property 0 :tagname release) nil nil nil 1)))))

              (add-text-properties 0 1 (list :target new) release)
              (when (stringp new)
                (save-excursion (goto-char (point-min))
                                (when (re-search-forward "^.*target: \\(?1:.*\\)?" header-end t)
                                  (replace-match (get-text-property 0 :target release) nil nil nil 1)))))
          (message "No release topic to edit!")))
    (message "Not in a release editing buffer!")))

(defun consult-gh-topics--edit-release-submit (release &optional title body tagname target draft latest prerelease discussion)
  "Edit RELEASE with new metadata.

Description of Arguments:
  RELEASE     a string: string with text properties that contain
              release metadata
  TITLE       a string; new title
  BODY        a string; new body
  TAGNAME     a string; new tag name
  TARGET      a string; new target branch/commit
  DRAFT       a boolean; whether to save release as draft
  LATEST      a boolean; whether to mark release as latest
  PRERELEASE  a boolean; whether to mark release as prerelease
  DISCUSSION  a string; discussion category for release"
  (pcase-let* ((repo (or (get-text-property 0 :repo release)
                         (get-text-property 0 :repo (consult-gh-search-repos nil t))))
               (canwrite (consult-gh--user-canwrite repo))
               (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
               (_ (unless canwrite
                    (user-error "Current user, %s, does not have permissions to edit this release" user)))
               (old-tagname (or (get-text-property 0 :tagname release)
                                (get-text-property 0 :tagname (consult-gh-release-list repo t))))
               (original-tagname (get-text-property 0 :original-tagname release))
               (original-target (get-text-property 0 :original-target release))
               (original-title (get-text-property 0 :original-title release))
               (original-body (get-text-property 0 :original-body release))
               (original-draft (get-text-property 0 :original-draft release))
               (original-prerelease (get-text-property 0 :original-prerelease release))
               (tagname (and (not (equal tagname original-tagname))
                             (stringp tagname)
                             tagname))
               (target (and (not (equal target original-target))
                            (stringp target)
                            target))

               (title (and (not (equal title original-title)) title))
               (body (and (not (equal body original-body)) body))
               (draft-changed (not (equal draft original-draft)))
               (prerelease-changed (not (equal prerelease original-prerelease)))
               (categories (if discussion (consult-gh--get-discussion-categories repo)))
               (discussion (if (and discussion
                                    (not (member discussion categories)))
                               (consult--read categories
                                              :prompt (format "%s category does not exist in discussios.  Select an existing one: " discussion))
                             discussion))
               (discussion (and (stringp discussion)
                                (not (string-empty-p discussion))
                                discussion))
               (args (list old-tagname "--repo" repo)))
    (when canwrite
      (cond
       ((or tagname target title body draft-changed prerelease-changed discussion)
        (setq args (delq nil (append args
                                     (and tagname (list "--tag" (substring-no-properties tagname)))
                                     (and target (list "--target" (substring-no-properties target)))
                                     (and title (list "--title" (substring-no-properties title)))
                                     (and body (list "--notes" (substring-no-properties body)))
                                     (if draft (list "--draft")
                                       (list "--draft=false"))
                                     (if latest (list "--latest")
                                       (list "--latest=false"))
                                     (if prerelease (list "--prerelease")
                                       (list "--prerelease=false"))
                                     (and discussion (list "--discussion-category" discussion)))))
        (apply #'consult-gh--command-to-string "release" "edit" args))
       (t (message "No Changes to submit")
          nil)))))

(defun consult-gh-topics--edit-release-presubmit (release)
  "Prepare edits on RELEASE to submit.

RELEASE is a string with properties that identify a github pull request.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--release-view'."
  (if consult-gh-topics-edit-mode
      (let* ((repo (get-text-property 0 :repo release))
             (tagname (get-text-property 0 :original-tagname release))
             (discussion (get-text-property 0 :original-discussion release))
             (canwrite (consult-gh--user-canwrite repo))
             (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
             (nextsteps (if canwrite
                            (append (list (cons "Submit" :submit))
                                    (list (cons "Change Tag Name" :tag))
                                    (list (cons "Change Title" :title))
                                    (list (cons "Change Body" :body))
                                    (list (cons "Change Target" :target))
                                    (list (cons "Discard edits and restore original values" :default))
                                    (list (cons "Cancel" :cancel)))
                          (user-error "Current user, %s, does not have permissions to edit this release" user)))
             (next (when nextsteps (consult--read nextsteps
                                                  :prompt "Choose what do you want to do? "
                                                  :lookup #'consult--lookup-cdr
                                                  :sort nil))))
        (when next
          (pcase-let* ((`(,title . ,body) (consult-gh-topics--get-title-and-body))
                       (title (or title
                                  (and (derived-mode-p 'org-mode)
                                       (cadar (org-collect-keywords
                                               '("title"))))
                                  ""))
                       (body (or body ""))
                       (metadata (when canwrite (consult-gh-topics--release-get-metadata)))
                       (new-tagname (when metadata (cdr (assoc "tagname" metadata))))
                       (target (when metadata (cdr (assoc "target" metadata))))
                       (draft (when metadata (cdr (assoc "draft" metadata))))
                       (prerelease (when metadata (cdr (assoc "prerelease" metadata))))
                       (draft (if (or (equal draft :false)
                                      (equal draft "false"))
                                  nil
                                draft))
                       (prerelease (if (or (equal prerelease :false)
                                           (equal prerelease "false"))
                                       nil
                                     prerelease))
                       (`(,new-tagname . ,target) (consult-gh--release-match-tag-and-target release new-tagname target)))

            (pcase next
              (':default (consult-gh-release--edit-restore-default))
              (':tag (consult-gh-release--edit-change-tagname nil tagname))
              (':title (consult-gh-release--edit-change-title nil title))
              (':target (consult-gh-release--edit-change-target nil target))
              (':body (consult-gh-release--edit-change-body body))
              (':submit
               (let* ((latest (y-or-n-p "Mark this as the latest release?"))
                      (discussion (and (not draft)
                                       (not discussion)
                                       (consult-gh--repo-has-discussions-enabled-p repo)
                                     (y-or-n-p "Would you like to add a discussion for this release?")
                                     (consult--read (consult-gh--get-discussion-categories repo)
                                                    :prompt "Select a discussion category: "
                                                    :require-match t
                                                    :sort nil)))
                    (discussion (and (stringp discussion)
                                     (not (string-empty-p discussion))
                                     discussion)))
               (and (consult-gh-topics--edit-release-submit release title body new-tagname target draft latest prerelease discussion)
                    (message "Edits %s" (propertize "Submitted!" 'face 'consult-gh-success))
                    (funcall consult-gh-quit-window-func t))))))))
    (message "% in a %s buffer!" (propertize "Not" 'face 'consult-gh-error) (propertize "pull request editing" 'face 'consult-gh-error))))

(defun consult-gh--workflow-format (string input highlight)
  "Format minibuffer candidates for workflows in `consult-gh-workflow-list'.

Description of Arguments:

  STRING    output of a “gh” call \(e.g. “gh workflow list ...”\).
  INPUT     a query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted with
            `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "workflow")
         (type "workflow")
         (parts (string-split string "\t"))
         (repo (car (consult--command-split input)))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (name (car parts))
         (state (cadr parts))
         (face (cond
                ((string-prefix-p "active" state) 'consult-gh-success)
                ((string-prefix-p "disabled" state) 'consult-gh-warning)
                ((string-prefix-p "deleted" state) 'consult-gh-issue)
                (t 'consult-gh-default)))
         (id (cadr (cdr parts)))
         (path (cadr (cddr parts)))
         (path-name (and path (stringp path) (file-name-nondirectory path)))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s\s%s\s\s%s\s\s%s"
                       (consult-gh--set-string-width (propertize (format "%s" name) 'face 'consult-gh-default) 40)
                       (propertize (consult-gh--set-string-width state 20) 'face face)
                       (consult-gh--set-string-width (propertize (format "%s" id) 'face 'consult-gh-description) 10)
                       (consult-gh--set-string-width (propertize (format "%s" path-name) 'face 'consult-gh-branch) 20)
                       (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user)) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :package package :state state :id id :path path :name path-name :query query :class class :type type) str)
    str))

(defun consult-gh--workflow-state ()
  "State function for workflow candidates.

This is passed as STATE to `consult--read' in `consult-gh-workflow-list'
and is used to preview or do other actions on the workflow."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (query (get-text-property 0 :query cand))
                        (name (get-text-property 0 :name cand))
                        (id (get-text-property 0 :id cand))
                        (match-str (consult--build-args query))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (consult-gh--workflow-view (format "%s" repo) (format "%s" id) buffer)
               (with-current-buffer buffer
                 (if consult-gh-highlight-matches
                     (cond
                      ((listp match-str)
                       (mapc (lambda (item)
                                 (highlight-regexp item 'consult-gh-preview-match))
                             match-str))
                      ((stringp match-str)
                       (highlight-regexp match-str 'consult-gh-preview-match)))))
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun consult-gh--workflow-group (cand transform)
  "Group function for workflow.

This is passed as GROUP to `consult--read' in `consult-gh-issue-list'
or `consult-gh-workflow-list', and is used to group workflows.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-workflows-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Name " 38 nil ?-)
       (consult-gh--set-string-width " State " 22 nil ?-)
       (consult-gh--set-string-width " ID " 12 nil ?-)
       (consult-gh--set-string-width " Path " 22 nil ?-)
       (consult-gh--set-string-width " Repo " 40 nil ?-))))))

(defun consult-gh--workflow-browse-url-action (cand)
  "Browse the url for a workflow action candidate, CAND.

This is an internal action function that gets a workflow candidate, CAND,
from `consult-gh-workflow-list' and opens the url of the workflow
in an external browser.

To use this as the default action for workflow,
set `consult-gh-workflow-action' to `consult-gh--workflow-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (path (substring-no-properties (get-text-property 0 :path cand)))
         (path-name (file-name-nondirectory path))
         (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")))

         (url (and repo-url (concat repo-url "/actions/workflows/" path-name))))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--workflow-read-json (repo workflow-id)
  "Get details of WORKFLOW-ID in REPO.

Runs an async shell command with the command:
gh api “/repos/REPO/actions/workflows/ID”,
and returns the output as a hash-table."
  (let* ((json-object-type 'hash-table)
         (json-array-type 'list)
         (json-key-type 'keyword)
         (json-false :false))
    (json-read-from-string (consult-gh--command-to-string "api" (format "/repos/%s/actions/workflows/%s" repo workflow-id)))))

(defun consult-gh--workflow-get-runs (repo workflow-id)
  "Get list of run instances for WORKFLOW-ID in REPO.

Runs an async shell command with the command:
gh api “/repos/REPO/actions/workflows/ID/runs”,
and returns the output as a hash-table."
  (consult-gh--json-to-hashtable (consult-gh--command-to-string "run" "list" "--workflow" workflow-id "--repo" repo "--json" "attempt,conclusion,createdAt,databaseId,displayTitle,event,headBranch,headSha,name,number,startedAt,status,updatedAt,url,workflowDatabaseId,workflowName")))

(defun consult-gh--workflow-get-runs-refs-history (repo workflow-id &optional runs)
"Get a list of refs for WORKFLOW-ID in REPO.

Optional arg RUNS is a list of previous runs of workflow.
This returns the name of refs for previous runs of a workflow."
  (let ((runs (or runs (consult-gh--workflow-get-runs repo workflow-id))))
        (when (listp runs)
          (mapcar (lambda (run) (when (hash-table-p run) (gethash :headBranch run))) runs))))

(defun consult-gh--workflow-get-yaml (repo workflow-id &optional ref sha)
  "Get yaml content for WORKFLOW-ID in REPO in REF.

REF is a string with branch name or tag name of the target
that contains the version of the workflow file to run.

SHA is the sha of the commit that was used for the last run.

Runs a shell command with the command:

gh workflow view  WORKFLOW-ID --repo REPO --yaml --ref REF"

  (if sha
      (when-let* ((path (consult-gh--json-to-hashtable (consult-gh--command-to-string "api" (format "/repos/%s/actions/workflows/%s" repo workflow-id)) :path)))
        (consult-gh--files-get-content-by-path repo path sha))
    (let ((args (list "workflow" "view" workflow-id "--repo" repo "--yaml")))
      (if (and (stringp ref)
               (not (string-empty-p ref)))
          (setq args (append args (list "--ref" ref))))
    (apply #'consult-gh--command-to-string args))))

(defun consult-gh--workflow-format-header (repo id-or-name runs &optional topic)
  "Format a header for ID-OR-NAME in REPO.

RUNS is a hash-table output containing runs information
from `consult-gh--workflow-get-runs'.  Returns a formatted string containing
the header section for `consult-gh--workflow-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--workflow-view'."
  (let* ((json (consult-gh--workflow-read-json repo id-or-name))
         (path (and (hash-table-p json)
                    (gethash :path json)))
         (path-name (and (stringp path)
                          (file-name-nondirectory path)))
         (id (and (hash-table-p json)
                    (format "%s" (gethash :id json))))
         (state (and (hash-table-p json)
                    (gethash :state json)))
         (yaml-url (and (hash-table-p json)
                    (gethash :html_url json)))
         (workflow-url (and path-name (stringp path-name) (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/actions/workflows/%s" path-name))))
         (run-count (when (listp runs) (length runs)))
         (first-run (when (listp runs) (car runs)))
         (title (when (hash-table-p first-run)
                  (gethash :workflowName first-run))))
    (when (stringp topic)
      (add-text-properties 0 1 (list :id id :path path :state state :name path-name :total-runs run-count :yaml-url yaml-url :url workflow-url) topic))

    (concat (and title (concat "title: " title "\n"))
            (and repo (concat "repository: " (propertize repo 'help-echo (apply-partially #'consult-gh--get-repo-tooltip repo)) "\n"))
            (and id (concat "id: " (propertize id 'face 'consult-gh-description) "\n"))
            (and path-name (concat "name: " path-name "\n"))
            (and path (concat "path: " path "\n"))
            (and workflow-url (concat "workflow-url: " workflow-url "\n"))
            (and yaml-url (concat "yaml-url: " yaml-url "\n"))
            (and (numberp run-count) (concat (format "runs: %s" run-count) "\n"))
            (and state (concat "state: " state))
            "\n--\n")))

(defun consult-gh--workflow-format-status (status conclusion)
  "Format STATUS and CONCLUSION for workflow run instances, jobs, ..."
(let* ((string (pcase status
                 ("completed" "✓")
                 ("canceled"  "x")
                 ("action_required" "!")
                 (_ status)))
       (face (pcase conclusion
               ("success" 'consult-gh-success)
               ("failure" 'consult-gh-error)
               (_ 'consult-gh-warning))))
       (propertize string 'face face)))

(defun consult-gh--workflow-format-runs (repo workflow-id &optional runs topic)
  "Format run instances for WORKFLOW-ID in REPO.

RUNS is a hash-table output containing workflow information
from `consult-gh--workflow-get-runs'.  Returns a formatted string containing
list of runs for `consult-gh--workflow-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--workflow-view'."
  (let* ((runs (or runs (consult-gh--workflow-get-runs repo workflow-id)))
         (ref-history (consult-gh--workflow-get-runs-refs-history repo workflow-id runs))
         (content (when (listp runs)
                    (cl-loop for run in runs
                             if (hash-table-p run)
                             collect (let* ((conclusion (gethash :conclusion run))
                                            (event (gethash :event run))
                                            (name (gethash :name run))
                                            (number (gethash :number run))
                                            (status (gethash :status run))
                                            (state (consult-gh--workflow-format-status status conclusion))
                                            (title (gethash :displayTitle run))
                                            (branch (gethash :headBranch run))
                                            (id (gethash :databaseId run))
                                            (url (gethash :url run))
                                            (startedAt (gethash :startedAt run))
                                            (updatedAt (gethash :updatedAt run))
                                            (elapsed (and startedAt updatedAt
                                                          (time-convert (time-subtract (date-to-time updatedAt)
                                                                                       (date-to-time startedAt)) 'integer)))
                                            (updatedAt (and updatedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time updatedAt))))
                                            (startedAt (and startedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time startedAt))))
                                            (start (consult-gh--time-ago startedAt)))
                                       (when (stringp topic)
                                         (add-text-properties 0 1 (list :ref-history ref-history) topic))
                                       (propertize (concat "## "
                                               (format "%s" number)
                                               ". "
                                               state
                                               "\s\s"
                                               (format "[[%s][%s]]" url title)
                                               (format "  [%s]  " branch)
                                               (format "(%s)" start)
                                               "\s"
                                               (format "%ss" elapsed)
                                               "\n"
                                               (and id (format "`ID:` %s\n" id))
                                               (and name (format "`NAME:` %s\n" name))
                                               (and branch (format "`BRANCH:` %s\n" branch))
                                               (and event (format "`EVENT:` %s\n" event))
                                               (and startedAt (format "`STARTED:` %s\n" startedAt))
                                               (and updatedAt (format "`UPDATED:` %s\n" startedAt))
                                               (and elapsed (format "`ELAPSED TIME:` %ss\n" elapsed))
                                               (and status (format "`STATUS:` %s - %s\n" status conclusion))
                                               (and url  (format "`URL:` %s\n" url))
                                               "\n")
                                                   :consult-gh (list :url url :run-id id)))))))
    (when (listp content)
      (concat "# Recent Runs\n" (string-join content) "\n"))))

(defun consult-gh--workflow-format-yaml (repo workflow-id &optional topic ref sha)
  "Format yaml content for WORKFLOW-ID in REPO and REF.

SHA is the Sha of the commit that was used for the last run.
Returns a formatted string containing
yaml file for `consult-gh--workflow-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--workflow-view'."
(let* ((yaml (consult-gh--workflow-get-yaml repo workflow-id ref sha))
       (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")))
       (run-info (consult-gh--command-to-string "api" (format "/repos/%s/actions/workflows/%s" repo workflow-id)))
       (html_url (consult-gh--json-to-hashtable run-info :html_url))
       (path (consult-gh--json-to-hashtable run-info :path))
       (url (if (and repo-url sha path)
                (format "%s/blob/%s/%s" repo-url sha path)
             html_url)))
  (when (stringp yaml)
    (when (stringp topic)
      (add-text-properties 0 1 (list :yaml yaml :yaml-url url) topic))
    (propertize (concat "# YAML Content"
                        (if ref (format " - From Last Run [%s]" ref))
                        (if sha (format " commit: %s" (substring sha 0 6)))
                        "\n\n"
                        "``` yaml\n"
                        yaml
                        "```\n")
                :consult-gh (list :yaml-url url :ref ref) :consult-gh-workflow-yaml t))))

(defun consult-gh--workflow-view (repo id-or-name &optional buffer)
  "Open workflow with ID-OR-NAME of REPO in an Emacs buffer, BUFFER.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and ID,
a workflow id of that repository, and shows
the sontents and actoin runs in an Emacs buffer.

It fetches the preview of the workflow by running the command
“gh workflow view ID --repo REPO” using `consult-gh--call-process'
and put it as raw text in either BUFFER or if BUFFER is nil,
in a buffer named by `consult-gh-preview-buffer-name'.
If `consult-gh-workflow-preview-major-mode' is non-nil, uses it as
major-mode, otherwise shows the raw text in \='fundamental-mode.

Description of Arguments:

  REPO       a string; the full name of the repository
  ID-OR-NAME a string; workflow id number or name
             (e.g. “170043631”, “action.yml”)
  BUFFER     a string; optional buffer name
  PREVIEW    a boolean; whether to load reduced preview

To use this as the default action for repos,
see `consult-gh--workflow-view-action'."
  (let* ((topic (format "%s/actions/%s" repo id-or-name))
         (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
         (runs (consult-gh--workflow-get-runs repo id-or-name))
         (runs-text (consult-gh--workflow-format-runs repo id-or-name runs topic))
         (header-text (consult-gh--workflow-format-header repo id-or-name runs topic))
         (last-run (car-safe runs))
         (ref (and (hash-table-p last-run)
                   (gethash :headBranch last-run)))
         (sha (and (hash-table-p last-run)
                   (gethash :headSha last-run)))
         (yaml-text (consult-gh--workflow-format-yaml repo id-or-name topic ref sha)))

    (add-text-properties 0 1 (list :repo repo :type "workflow" :view "workflow" :last-ref ref) topic)

    (with-current-buffer buffer
      (let ((inhibit-read-only t))
        (erase-buffer)
        (fundamental-mode)
        (when header-text
          (insert header-text)
          (save-excursion
          (when (eq consult-gh-workflow-preview-major-mode 'org-mode)
           (consult-gh--github-header-to-org buffer))))
        (when runs-text
          (insert runs-text))
        (when yaml-text
          (insert yaml-text))
        (consult-gh--format-view-buffer "workflow")
        (outline-hide-sublevels 1)
        (consult-gh-workflow-view-mode +1)
        (setq-local consult-gh--topic topic)
        (current-buffer)))))

(defun consult-gh--workflow-view-action (cand)
  "Open the preview of a workflow candidate, CAND.

This is a wrapper function around `consult-gh--workflow-view'.
It parses CAND to extract relevant values
\(e.g. repository's name and workflow id\)
and passes them to `consult-gh--workflow-view'.

To use this as the default action for workflows,
set `consult-gh-workflow-action' to `consult-gh--workflow-view-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (id (substring-no-properties (format "%s" (get-text-property 0 :id cand))))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/workflows/" id "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the workflow in the existing buffer." :replace)
                             (cons "Make a new buffer and load the workflow in it (without killing the old buffer)." :new))
                       :prompt "You already have this workflow open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))

(if existing
      (cond
       ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
       ((eq confirm :replace)
        (message "Reloading action in the existing buffer...")
        (funcall consult-gh-switch-to-buffer-func (consult-gh--workflow-view repo id existing))
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer)))
       ((eq confirm :new)
        (message "Opening action in a new buffer...")
        (funcall consult-gh-switch-to-buffer-func (consult-gh--workflow-view repo id (generate-new-buffer buffername nil)))
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--workflow-view repo id))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--workflow-run (repo id-or-name &optional ref ref-history)
  "Run workflow with ID-OR-NAME of REPO in REF branch/tag.

REF defaults to REPO's main branch.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and ID-OR-NAME,
a workflow id of that repository, runs the workflow.

Description of Arguments:

  REPO       a string; the full name of the repository
  ID-OR-NAME a string; workflow id number or name
             (e.g. “170043631”, “action.yml”)
  REF        a string; Branch or tag name which contains
             the version of the workflow file to run

To use this as the default action for repos,
see `consult-gh--workflow-view-action'."
  (let* ((ref-history (or ref-history (consult-gh--workflow-get-runs-refs-history repo id-or-name)))
         (ref (or ref (consult-gh--read-ref repo nil "Select Branch or tag name with the version of the workflow to run: " t nil (if ref-history 'ref-history t))))
         (args (list "workflow" "run" (format "%s" id-or-name) "--repo" repo))
         (args (if (and (stringp ref)
                        (not (string-empty-p ref)))
                   (append args (list "--ref" (substring-no-properties ref)))
                 args))
         (yaml (consult-gh--workflow-get-yaml repo id-or-name (substring-no-properties ref)))
         (yaml--parsing-object-type 'hash-table)
         (yaml-table (yaml-parse-string yaml))
         (dispatch (and (hash-table-p yaml-table) (map-nested-elt yaml-table '(on workflow_dispatch))))
         (inputs (and (hash-table-p dispatch) (gethash 'inputs dispatch)))
         (keys (and (hash-table-p inputs) (hash-table-keys inputs)))
         (_ (when (listp keys)
              (cl-loop for key in keys
                       do
                       (let ((val (read-string (format "Enter value for \"%s\": " key))))
                         (setq args (append args (list "-f" (format "%s=%s" key val)))))))))
       (consult-gh--make-process (format "consult-gh-workflow-run-%s-%s" repo id-or-name)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args)))

(defun consult-gh--workflow-run-action (cand)
  "Run a workflow candidate, CAND.

This is a wrapper function around `consult-gh--workflow-run.
It parses CAND to extract relevant values
\(e.g. repository's name and workflow id\)
and passes them to `consult-gh--workflow-run'.

To use this as the default action for workflows,
set `consult-gh-workflow-action' to `consult-gh--workflow-run-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (id (substring-no-properties (format "%s" (get-text-property 0 :id cand)))))
         (consult-gh--workflow-run repo id)))

(defun consult-gh--workflow-enable (repo id-or-name)
  "Enable workflow with ID-OR-NAME of REPO.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and ID-OR-NAME,
a workflow id of that repository, and enables the workflow.

Description of Arguments:

  REPO       a string; the full name of the repository
  ID-OR-NAME a string; workflow id number or name
             (e.g. “170043631”, “action.yml”)

To use this as the default action for repos,
see `consult-gh--workflow-view-action'."
  (let* ((canwrite (consult-gh--user-canwrite repo))
         (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (_ (unless canwrite
              (user-error "The current user, %s, %s to enable a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
         (args (list "workflow" "enable" id-or-name "--repo" repo)))
    (consult-gh--make-process (format "consult-gh-workflow-enable-%s-%s" repo id-or-name)
                              :when-done (lambda (_ str) (message str))
                              :cmd-args args)))

(defun consult-gh--workflow-disable (repo id-or-name)
  "Disable workflow with ID-OR-NAME of REPO.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and ID-OR-NAME,
a workflow id of that repository, and enables the workflow.

Description of Arguments:

  REPO       a string; the full name of the repository
  ID-OR-NAME a string; workflow id number or name
             (e.g. “170043631”, “action.yml”)

To use this as the default action for repos,
see `consult-gh--workflow-view-action'."
  (let* ((canwrite (consult-gh--user-canwrite repo))
         (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
         (_ (unless canwrite
              (user-error "The current user, %s, %s to enable a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
         (args (list "workflow" "disable" id-or-name "--repo" repo)))
    (consult-gh--make-process (format "consult-gh-workflow-enable-%s-%s" repo id-or-name)
                              :when-done (lambda (_ str) (message str))
                              :cmd-args args)))

(defun consult-gh--workflow-enable-toggle-action (cand)
  "Toggle a workflow candidate, CAND, enabled or disabled.

This is a wrapper function around `consult-gh--workflow-enable'
and `consult-gh--workflow-disable'.
It parses CAND to extract relevant values
\(e.g. repository's name and workflow id\)
and passes them to `consult-gh--workflow-enable'
or `consult-gh--workflow-disable' depending on the current state.

To use this as the default action for workflows,
set `consult-gh-workflow-action' to `consult-gh--workflow-enable-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (id (substring-no-properties (format "%s" (get-text-property 0 :id cand))))
         (state (substring-no-properties (format "%s" (get-text-property 0 :state cand)))))
    (if (equal state "active")
        (consult-gh--workflow-disable repo id)
        (consult-gh--workflow-enable repo id))))

(defun consult-gh--workflow-edit-yaml (repo workflow-path &optional ref)
  "Edit YAML content of WORKFLOW-PATH in REPO and REF branch/tag."
  (let* ((ref (or (consult-gh--read-ref repo nil "Select ref branch or tag name (Enter for HEAD): " t nil t ref nil)
                  "HEAD"))
         (newtopic (format "%s/%s/%s" repo ref workflow-path)))
    (add-text-properties 0 1 (list :repo repo :type "file" :path workflow-path :ref ref :object-type "blob" :mode "file" :title nil :url nil :changed-locally nil) newtopic)
    (consult-gh-edit-file newtopic)))

(defun consult-gh--workflow-edit-yaml-action (cand)
  "Edit yaml content of workflow, CAND.

This is a wrapper function around
`consult-gh--workflow-edit-yaml'.
It parses CAND to extract relevant values
\(e.g. repository's name, workflow id, etc.\)
and passes them to `consult-gh--workflow-edit-yaml'.

To use this as the default action for workflows,
set `consult-gh-workflow-action' to `consult-gh--workflow-edit-yaml-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (path (substring-no-properties (get-text-property 0 :path cand)))
         (ref (or (get-text-property 0 :ref cand)
                  (get-text-property 0 :last-ref cand))))
         (consult-gh--workflow-edit-yaml repo path ref)))

(defun consult-gh--workflow-get-templates-state ()
  "State function for workflow templates.

This is passed as STATE to a `consult--multi'
source in `consult-gh--workflow-get-templates'
and is used to preview YAML files."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview (listp cand))
             (let* ((path (plist-get cand :path)))
               (cond
                ((stringp path)
                 (let* ((repo (plist-get cand :repo))
                        (ref (or (plist-get cand :ref) "HEAD"))
                        (size (plist-get cand :size))
                        (object-type (plist-get cand :object-type))
                        (tempdir (expand-file-name (concat repo "/" ref "/")
                                                   (or consult-gh--current-tempdir
                                                       (consult-gh--tempdir))))
                        (file-p (equal object-type "blob"))
                        (file-size (and file-p size))
                        (confirm (if (and file-size (>= file-size
                                                        consult-gh-large-file-warning-threshold))
                                     (yes-or-no-p (format "File is %s Bytes.  Do you really want to load it?" file-size))
                                   t))
                        (temp-file (or (cdr (assoc (substring-no-properties (concat repo "/" "path")) consult-gh--open-files-list)) (expand-file-name path tempdir)))
                        (_ (and file-p confirm (progn
                                                 (unless (file-exists-p temp-file)
                                                   (make-directory (file-name-directory temp-file) t)
                                                   (with-temp-file temp-file
                                                     (setq-local after-save-hook nil)
                                                     (insert (consult-gh--files-get-content-by-path repo path ref))
                                                     (set-buffer-file-coding-system 'raw-text)
                                                     (set-buffer-multibyte t)
                                                     (let ((after-save-hook nil))
                                                       (write-file temp-file))
                                                     (set-buffer-modified-p nil)))
                                                 (add-to-list 'consult-gh--open-files-list `(,(substring-no-properties (concat repo "/" path)) . ,temp-file)))))
                        (buffer (or (and file-p confirm (find-file-noselect temp-file t)) nil)))
                   (add-to-list 'consult-gh--preview-buffers-list buffer)
                   (add-to-list 'consult-gh--open-files-buffers buffer)
                   (funcall preview action
                            buffer)))))))))))

(defun consult-gh--workflow-get-stater-workflows (input)
  "Search starter-workflows for INPUT."
  (let* ((files (consult-gh--files-list-items "actions/starter-workflows" nil nil nil "2m"))
         (workflows (and (listp files)
                          (remove nil
                                  (mapcar
                                   (lambda (item) (if (and
                                                       (string-match ".*.yml" (car item))
                                                       (not (string-match "\.github" (car item)))
                                                       (string-match (format ".*%s.*" input) (car item)))
                                                               item))
                                   files)))))
    workflows))

(defun consult-gh--workflow-get-repo-workflows (repo input)
  "Search REPO workflows for INPUT."
  (let* ((files (consult-gh--files-list-items repo nil nil nil "30s"))
         (input (or input ""))
         (workflows (and (listp files)
                         (remove nil
                                 (mapcar
                                  (lambda (item) (if (and  (listp item)
                                                           (stringp (car-safe item))
                                                           (string-match "\.github/workflows/.*.yml" (car-safe item))
                                                           (string-match (format ".*%s.*" input) (car-safe item)))
                                                     item))
                                  files)))))
    workflows))

(defun consult-gh--workflow-get-templates (&optional repo)
  "Get workflow templates for REPO."
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo-workflows (if repo (lambda (input) (funcall #'consult-gh--workflow-get-repo-workflows repo input))))
          (starter #'consult-gh--workflow-get-stater-workflows)
          (sources (append (list (list :name "Set Up a Workflow Yourself"
                                       :items (list (list "Make a New YAML File" :repo repo :path 'read))
                                       :state #'consult-gh--workflow-get-templates-state))
                      (if repo-workflows (list (list :name repo
               :async (consult--dynamic-collection repo-workflows :min-input 0 :highlight t)
               :state #'consult-gh--workflow-get-templates-state)))
                      (list (list :name "Starter Workflows"
                          :async (consult--dynamic-collection starter :min-input 0 :highlight t)
                          :state #'consult-gh--workflow-get-templates-state))
                    (if (listp consult-gh-workflow-template-repo-sources)
                        (mapcar (lambda (item)
                                  (if (stringp item)
                                      (list :name item
               :async (consult--dynamic-collection (lambda (input) (funcall #'consult-gh--workflow-get-repo-workflows item input)) :min-input 0 :highlight t)
               :state #'consult-gh--workflow-get-templates-state)))
                                consult-gh-workflow-template-repo-sources)))))

   (consult--multi
    (remove nil (cl-remove-duplicates sources :test #'equal))
    :prompt "Select A Template Workflow: "
    :sort nil
    :preview-key consult-gh-preview-key))))

(defun consult-gh--run-format (string input highlight)
  "Format minibuffer candidates for actions run instances.

This is used to format candidates in `consult-gh-run-list'.

Description of Arguments:

  STRING    output of a “gh” call \(e.g. “gh run list ...”\).
  INPUT     a query from the user
            \(a.k.a. command line argument passed to the gh call\).
  HIGHLIGHT if non-nil, input is highlighted with
            `consult-gh-highlight-match' in the minibuffer."
  (let* ((class "run")
         (type "run")
         (parts (string-split string "\t"))
         (repo (car (consult--command-split input)))
         (user (consult-gh--get-username repo))
         (package (consult-gh--get-package repo))
         (name (car parts))
         (state (cadr parts))
         (conclusion (cadr (cdr parts)))
         (face (cond
                ((string-prefix-p "completed" state) 'consult-gh-success)
                ((or (string-prefix-p "canceled" state) (string-prefix-p "failure" state))
                 'consult-gh-issue)
                (t 'consult-gh-warning)))
         (id (cadr (cddr parts)))
         (ref (cadr (cdddr parts)))
         (event (cadr (cdddr (cdr parts))))
         (startedAt (cadr (cdddr (cddr parts))))
         (updatedAt (cadr (cdddr (cdddr parts))))
         (elapsed (and startedAt updatedAt
                       (time-convert (time-subtract (date-to-time updatedAt)
                                                    (date-to-time startedAt)) 'integer)))
         (updatedAt (and updatedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time updatedAt))))
         (startedAt (and startedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time startedAt))))
         (age (consult-gh--time-ago startedAt))
         (workflow-name (cadr (cdddr (cdddr (cdr parts)))))
         (workflow-id (cadr (cdddr (cdddr (cddr parts)))))
         (query input)
         (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
         (str (format "%s\s\s%s\s%s\s%s\s%s\s%s\s%s\s%s\s\s%s"
                       (consult-gh--set-string-width (propertize (format "%s" name) 'face 'consult-gh-default) 35)
                       (propertize (consult-gh--set-string-width state 12) 'face face)
                       (consult-gh--set-string-width (propertize workflow-name 'face 'consult-gh-default) 12)
                       (consult-gh--set-string-width (propertize ref 'face 'consult-gh-branch) 8)
                       (consult-gh--set-string-width (propertize event 'face 'consult-gh-date) 12)
                       (consult-gh--set-string-width (propertize (format "%s" id) 'face 'consult-gh-description) 12)
                       (consult-gh--set-string-width (propertize (format "%ss" elapsed) 'face 'consult-gh-branch) 9)
                       (consult-gh--set-string-width (propertize (format "%s" age) 'face 'consult-gh-branch) 12)
                       (consult-gh--set-string-width (concat (and user (propertize user 'face 'consult-gh-user)) (and package "/") (and package (propertize package 'face 'consult-gh-package))) 40))))
    (if (and consult-gh-highlight-matches highlight)
        (cond
         ((listp match-str)
          (mapc (lambda (match) (setq str (consult-gh--highlight-match match str t))) match-str))
         ((stringp match-str)
          (setq str (consult-gh--highlight-match match-str str t)))))
    (add-text-properties 0 1 (list :repo repo :user user :package package :state state :conclusion conclusion :id id :workflow workflow-name :workflow-id workflow-id :event event :ref ref :startedAt startedAt :updatedAt updatedAt :elapsed elapsed :age age :query query :class class :type type) str)
    str))

(defun consult-gh--run-state ()
  "State function for run candidates.

This is passed as STATE to `consult--read' in `consult-gh-workflow-list'
and is used to preview or do other actions on the run."
  (lambda (action cand)
    (let* ((preview (consult--buffer-preview)))
      (pcase action
        ('preview
         (if (and consult-gh-show-preview cand)
             (when-let* ((repo (get-text-property 0 :repo cand))
                        (query (get-text-property 0 :query cand))
                        (name (get-text-property 0 :name cand))
                        (id (get-text-property 0 :id cand))
                        (match-str (consult--build-args query))
                        (buffer (get-buffer-create consult-gh-preview-buffer-name)))
               (add-to-list 'consult-gh--preview-buffers-list buffer)
               (consult-gh--run-view (format "%s" repo) (format "%s" id) buffer)
               (with-current-buffer buffer
                 (if consult-gh-highlight-matches
                     (cond
                      ((listp match-str)
                       (mapc (lambda (item)
                                 (highlight-regexp item 'consult-gh-preview-match)) match-str))
                      ((stringp match-str)
                       (highlight-regexp match-str 'consult-gh-preview-match)))))
               (funcall preview action
                        buffer))))
        ('return
         cand)))))

(defun consult-gh--run-group (cand transform)
  "Group function for run.

This is passed as GROUP to `consult--read' in `consult-gh-issue-list'
or `consult-gh-workflow-list', and is used to group runs.

If TRANSFORM is non-nil, the CAND itself is returned."
  (let* ((name (consult-gh--group-function cand transform consult-gh-group-runs-by)))
    (cond
     ((stringp name) name)
     ((equal name t)
      (concat
       (consult-gh--set-string-width "Name " 33 nil ?-)
       (consult-gh--set-string-width " State " 13 nil ?-)
       (consult-gh--set-string-width " Workflow " 13 nil ?-)
       (consult-gh--set-string-width " Ref " 9 nil ?-)
       (consult-gh--set-string-width " Event " 13 nil ?-)
       (consult-gh--set-string-width " ID " 13 nil ?-)
       (consult-gh--set-string-width " Elapsed " 10 nil ?-)
       (consult-gh--set-string-width " Age " 14 nil ?-)
       (consult-gh--set-string-width " Repo " 40 nil ?-))))))

(defun consult-gh--run-browse-url-action (cand)
  "Browse the url for an action run candidate, CAND.

This is an internal action function that gets a run candidate, CAND,
from `consult-gh-run-list' and opens the url of the run
in an external browser.

To use this as the default action for run,
set `consult-gh-run-action' to `consult-gh--run-browse-url-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (id (substring-no-properties (get-text-property 0 :id cand)))
         (repo-url (string-trim (consult-gh--command-to-string "browse" "--repo" repo "--no-browser")))

         (url (and repo-url (concat repo-url "/actions/runs/" id))))
    (funcall (or consult-gh-browse-url-func #'browse-url) url)))

(defun consult-gh--run-read-json (repo run-id)
  "Get details of RUN-ID in REPO.

Runs an async shell command with the command:
gh run view RUN-ID,
and returns the output as a hash-table."
  (let* ((json-object-type 'hash-table)
         (json-array-type 'list)
         (json-key-type 'keyword)
         (json-false :false))
    (json-read-from-string (consult-gh--command-to-string "api" (format "/repos/%s/actions/runs/%s" repo run-id)))))

(defun consult-gh--run-get-jobs (repo run-id)
  "Get list of jobs for RUN-ID in REPO.

Runs an async shell command with the command:
gh run view RUN-ID,
and returns the output as a hash-table."
  (consult-gh--json-to-hashtable (consult-gh--command-to-string "run" "view" run-id "--repo" repo "--json" "jobs") :jobs))

(defun consult-gh--run-get-log (repo run-id)
  "Get log for RUN-ID in REPO."
 (consult-gh--command-to-string "run" "view" run-id "--repo" repo "--log" "--verbose"))

(defun consult-gh--run-format-header (repo run-id &optional table topic)
  "Format a body for RUN-ID in REPO.

TABLE is a hash-table output containing information
from `consult-gh--run-get-jobs'.  Returns a formatted string containing
the header section for `consult-gh--run-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--run-view'."
  (let* ((table (or (and (hash-table-p table) table)
                    (consult-gh--run-read-json repo run-id)))
         (status (gethash :status table))
         (conclusion (gethash :conclusion table))
         (state (consult-gh--workflow-format-status status conclusion))
         (branch (gethash :head_branch table))
         (branch (and (stringp branch) (propertize branch 'face 'consult-gh-branch)))
         (title (gethash :display_title table))
         (url (gethash :html_url table))
         (path (gethash :path table))
         (path-name (and path (file-name-nondirectory path)))
         (workflow-id (gethash :workflow_id table))
         (workflow-url (and path-name (stringp path-name) (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/actions/workflows/%s" path-name))))
         (actor (gethash :login (or (gethash :triggering_actor table) (gethash :actor table))))
         (actor (and (stringp actor) (propertize actor 'face 'consult-gh-user)))
         (actor (and (stringp actor)
                                     (propertize actor 'help-echo (apply-partially #'consult-gh--get-user-tooltip actor) 'rear-nonsticky t)))
         (event (gethash :event table))
         (startedAt (gethash :run_started_at table))
         (updatedAt (gethash :updated_at table))
         (elapsed (and startedAt updatedAt
                       (time-convert (time-subtract (date-to-time updatedAt)
                                                    (date-to-time startedAt)) 'integer)))
         (updatedAt (and updatedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time updatedAt))))
         (startedAt (and startedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time startedAt))))
         (age (consult-gh--time-ago startedAt)))

     (when (stringp topic)
      (add-text-properties 0 1 (list :workflow-id workflow-id :workflow-url workflow-url :workflow-name path-name :path path :status status :conclusion conclusion  :actor actor :event event :url url) topic))

      (concat (and title (concat "title: " title "\n"))
              (and repo (concat "repository: " (propertize repo 'help-echo (apply-partially #'consult-gh--get-repo-tooltip repo)) "\n"))
            (and run-id (concat "id: " (propertize run-id 'face 'consult-gh-description) "\n"))
            (and path-name (format "workflow: %s(%s)\n" path-name workflow-id))
            (and workflow-url (format "workflow_url: %s\n" workflow-url))
            (and url (format "run_url: %s\n" url))
            (and startedAt (concat "started: " startedAt "\n"))
            (and updatedAt (concat "updated: " updatedAt "\n"))
            (and elapsed (format "run_time: %ss\n" elapsed))
            (and status (concat "status: " status "\n"))
            (and conclusion (concat "conclusion: " status "\n"))
            "\n--\n"
            state
            "\s"
            (and branch (concat "[" branch "]" "\s"))
            (and title (concat title "\s"))
            (and url (format ". [%s](%s)" run-id url))
            "\n\n"
            (format "Trigerred via %s about %s by %s and took %ss\n" event age actor elapsed))))

(defun consult-gh--run-format-job-steps (job)
  "Format step section of JOB."
  (when (hash-table-p job)
    (let* ((steps (gethash :steps job))
           (steps-list (when (listp steps)
                         (remove nil (cl-loop for step in steps
                                              collect
                                              (let* ((name (gethash :name step))
                                                     (number (gethash :number step))
                                                     (status (gethash :status step))
                                                     (conclusion (gethash :conclusion step))
                                                     (state (consult-gh--workflow-format-status status conclusion))
                                                     (startedAt (gethash :startedAt step))
                                                     (completedAt (gethash :completedAt step))
                                                     (elapsed (and startedAt completedAt
                                                                   (time-convert (time-subtract (date-to-time completedAt)
                                                                                                (date-to-time startedAt)) 'integer)))
                                                     (completedAt (and completedAt (format-time-string "[%Y-%m-%d %H:%M:%S]" (date-to-time completedAt)))))

                                                (concat  (format "%s" number) ". " name "\t" state "\t" (format "completed at %s" completedAt) "\s" (format "%ss" elapsed) "\n")))))))
      (when (listp steps-list) (string-join steps-list "\n")))))

(defun consult-gh--run-format-jobs (repo run-id &optional topic)
  "Format jobs for RUN-ID in REPO.

Returns a formatted string containing
list of jobs in a run for `consult-gh--run-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--workflow-view'."
(let* ((jobs (consult-gh--run-get-jobs repo run-id))
       (content (cl-loop for job in jobs
                         collect
                         (let* ((name (gethash :name job))
                                (id (gethash :databaseId job))
                                (status (gethash :status job))
                                (conclusion (gethash :conclusion job))
                                (state  (consult-gh--workflow-format-status status conclusion))
                                (steps (consult-gh--run-format-job-steps job)))
                                (propertize (concat "## "
                                        state
                                        "\t"
                                        (format "%s (%s)" name id)
                                        "\n"
                                        steps)
                                            :consult-gh (list :run-id run-id
                                            :job-id id))))))
  (when (and (listp jobs) (stringp topic))
      (add-text-properties 0 1 (list :jobs jobs) topic))
  (when (listp content) (concat "# Jobs\n" (string-join content "\n") "\n"))))

(defun consult-gh--run-format-log (repo run-id &optional topic)
  "Format log content for RUN-ID in REPO.

Returns a formatted string containing
log of RUN-ID for `consult-gh--run-view'.

The optional argument TOPIC is a propertized text where the related info
from the header will get appended to the properties.  For an example, see
the buffer-local variable `consult-gh--topic' in the buffer created by
`consult-gh--run-view'."
(let* ((log (consult-gh--run-get-log repo run-id)))
  (when (stringp log)
    (when (stringp topic)
      (add-text-properties 0 1 (list :log log) topic))
   (concat "# Log\n\n"
            "``` log\n"
            log
            "```\n"))))

(defun consult-gh--run-view (repo run-id &optional buffer)
  "Open run with RUN-ID of REPO in an Emacs buffer, BUFFER.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and RUN-ID,
an action run id of that repository, and shows
the run details in an Emacs buffer.

It fetches the preview of the run from `consult-gh--run-get-jobs',
and put it as raw text in either BUFFER or if BUFFER is nil,
in a buffer named by `consult-gh-preview-buffer-name'.
If `consult-gh-run-preview-major-mode' is non-nil, uses it as
major-mode, otherwise shows the raw text in \='fundamental-mode.

Description of Arguments:

  REPO    a string; the full name of the repository
  RUN-ID  a string; run id number
  BUFFER  a string; optional buffer name

To use this as the default action for runs,
see `consult-gh--run-view-action'."
  (let* ((topic (format "%s/actions/run/%s" repo run-id))
         (buffer (or buffer (get-buffer-create consult-gh-preview-buffer-name)))
         (json (consult-gh--run-read-json repo run-id))
         (header-text (consult-gh--run-format-header repo run-id json topic))
         (jobs-text (consult-gh--run-format-jobs repo run-id topic))
         (log-text (consult-gh--run-format-log repo run-id topic)))

    (add-text-properties 0 1 (list :repo repo :type "run" :id run-id :view "run") topic)

    (with-current-buffer buffer
      (let ((inhibit-read-only t))
        (erase-buffer)
        (fundamental-mode)
        (when header-text
          (insert header-text)
          (save-excursion
          (when (eq consult-gh-run-preview-major-mode 'org-mode)
           (consult-gh--github-header-to-org buffer)))
          (insert "\n"))
        (when jobs-text
          (insert jobs-text))
        (when log-text
          (insert log-text))
        (consult-gh--format-view-buffer "run")
        (outline-hide-sublevels 1)
        (consult-gh-run-view-mode +1)
        (setq-local consult-gh--topic topic)
        (current-buffer)))))

(defun consult-gh--run-view-action (cand)
  "Open the preview of a run candidate, CAND.

This is a wrapper function around `consult-gh--run-view'.
It parses CAND to extract relevant values
\(e.g. repository's name and run id\)
and passes them to `consult-gh--run-view'.

To use this as the default action for workflows,
set `consult-gh-run-action' to `consult-gh--run-view-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (id (substring-no-properties (format "%s" (get-text-property 0 :id cand))))
         (buffername (concat (string-trim consult-gh-preview-buffer-name "" "*") ":" repo "/runs/" id "*"))
         (existing (get-buffer buffername))
         (confirm (if (and existing (not (= (buffer-size existing) 0)))
                      (consult--read
                       (list (cons "Switch to existing buffer." :resume)
                             (cons "Reload the workflow in the existing buffer." :replace)
                             (cons "Make a new buffer and load the run in it (without killing the old buffer)." :new))
                       :prompt "You already have this run open in another buffer.  Would you like to switch to that buffer or make a new one? "
                       :lookup #'consult--lookup-cdr
                       :sort nil
                       :require-match t))))

    (if existing
        (cond
         ((eq confirm :resume) (funcall consult-gh-switch-to-buffer-func existing))
         ((eq confirm :replace)
          (message "Reloading action in the existing buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--run-view repo id existing))
          (set-buffer-modified-p nil)
          (buffer-name (current-buffer)))
         ((eq confirm :new)
          (message "Opening action in a new buffer...")
          (funcall consult-gh-switch-to-buffer-func (consult-gh--run-view repo id (generate-new-buffer buffername nil)))
          (set-buffer-modified-p nil)
          (buffer-name (current-buffer))))
      (progn
        (funcall consult-gh-switch-to-buffer-func (consult-gh--run-view repo id))
        (rename-buffer buffername t)
        (set-buffer-modified-p nil)
        (buffer-name (current-buffer))))))

(defun consult-gh--run-rerun (repo run-id &optional job-id)
  "Rerun run with RUN-ID of REPO.

This is an internal function that takes REPO, the full name of a
repository \(e.g. “armindarvish/consult-gh”\) and RUN-ID,
an action run id of that repository, and reruns it using
“gh run rerun”.

If JOB-ID is non-nil only reruns the specific job with:
“gh run rerun --job JOB-ID”

Description of Arguments:

  REPO    a string; the full name of the repository
  RUN-ID  a string; run id number
  JOB-ID  a string: job id

To use this as the default action for runs,
see `consult-gh--run-view-action'."
  (let* ((args (list "run" "rerun" (format "%s" run-id) "--repo" repo))
         (args (if (and job-id
                        (stringp job-id)
                        (not (string-empty-p job-id)))
                   (append args (list "--job" job-id))
                 args)))
    (consult-gh--make-process (format "consult-gh-run-rerun-%s-%s" repo run-id)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args)))

(defun consult-gh--run-rerun-action (cand)
  "Rerun a run candidate, CAND.

This is a wrapper function around `consult-gh--run-rerun'.
It parses CAND to extract relevant values
\(e.g. repository's name and run id\)
and passes them to `consult-gh--run-rerun'.

To use this as the default action for workflows,
set `consult-gh-run-action' to `consult-gh--run-rerun-action'."
  (let* ((repo (substring-no-properties (get-text-property 0 :repo cand)))
         (id (substring-no-properties (format "%s" (get-text-property 0 :id cand))))
         (job-id (get-text-property 0 :job-id cand))
         (job-id (if (and job-id (stringp job-id) (not (string-empty-p job-id)))
                     (format "%s" job-id)
                   job-id)))
    (consult-gh--run-rerun repo id job-id)))

(defun consult-gh-dired--get-files-list (repo &optional path ref)
  "Get a list of files for REPO, PATH, and REF."
(let* ((files-list (consult-gh--files-list-items repo path ref nil)))
  (if (and path (stringp path))
      (append (list (list ".." :repo repo :ref ref :api-url nil :path (and (stringp path) (file-name-directory path)) :size nil :object-type "tree" :new nil :mode "040000" :type "directory" :sha nil))
              files-list)
    (append (list (list "." :repo repo :ref ref :api-url nil :path nil :size nil :object-type "tree" :new nil :mode "040000" :type "directory" :sha nil))
            files-list))))

(defun consult-gh-dired--files-list-to-string (files-list)
  "Convert FILES-LIST to a string of sorted file names."
  (when (listp files-list)
    (mapconcat #'identity
               (sort files-list
                     (lambda (x y)
                       (let ((x-parent (or (get-text-property 0 :parent x) ""))
                             (y-parent (or (get-text-property 0 :parent y) "")))
                         (string< x-parent y-parent))))

               "\n")))

(defun consult-gh-dired-line-marked-p (&optional pos)
  "Check whether line at POS is marked.

POS defaults to the current point."
  (save-excursion
    (when pos (goto-char pos))
    (string-match-p "\*\s" (buffer-substring-no-properties (line-beginning-position) (line-end-position)))))

(defun consult-gh-dired--hidden-p (&optional pos)
  "Whether the character at POS is hidden.

POS defaults to the current point."
  (eq (get-char-property (or pos (point)) 'invisible) 'consult-gh-dired))

(defun consult-gh--dired-map-over-marks (fun)
  "Apply FUN on each marked line."
  (if (derived-mode-p 'consult-gh-dired-mode)
      (save-excursion
        (goto-char (point-min))
        (cl-loop while (consult-gh-dired-next-marked-file)
                 collect
                 (funcall fun)))))

(defun consult-gh--dired-get-all-mark-position ()
  "Get the position of all marked lines."
  (if (derived-mode-p 'consult-gh-dired-mode)
      (save-excursion
        (goto-char (point-min))
        (consult-gh--dired-map-over-marks #'point))))

(defun consult-gh--dired-get-all-files-in-directory ()
  "Get all files under the directory at the current line."
  (save-excursion
    (consult-gh--dired-goto-prev-directory-header)
    (let* ((parent (get-text-property (point) :parent))
           (files (cl-loop while (and (< (point) (point-max))
                               (equal parent (get-text-property (point) :parent)))
                    do (forward-line +1)
                    collect  (if (stringp parent)
                                 (string-remove-prefix parent (get-text-property (point) :path))
                               (get-text-property (point) :path)))))
           (cl-remove-duplicates (remove nil files) :test #'equal))))

(defun consult-gh--dired-file-name-position (&optional pos)
  "Get the position of beginning of the file name at POS.

POS defaults to current point."
  (save-excursion
    (when pos (goto-char pos))
    (goto-char (line-beginning-position))
    (next-single-property-change (point) :consult-gh-dired-file-name-begin nil (line-end-position))))

(defun consult-gh--dired-goto-file-name-position (&optional pos)
  "Go to the position of beginning of the file name at POS.

POS defaults to current point."
  (when-let* ((p (consult-gh--dired-file-name-position pos)))
    (goto-char p)))

(defun consult-gh--dired-hide (start end)
"Hide region from START to END."
(save-excursion
    (put-text-property (progn (goto-char start) (line-end-position))
                       (progn (goto-char end) (line-end-position))
                       'invisible 'consult-gh-dired)))

(defun consult-gh--dired-unhide (start end)
  "Unide region from START to END."
  (save-excursion
    (let ((inhibit-read-only t))
      (remove-list-of-text-properties
       (progn (goto-char start) (line-end-position))
       (progn (goto-char end) (line-end-position))
       '(invisible)))))

(defun consult-gh--dired-mark-line (&optional pos)
  "Mark the line at POS.

POS defaults to current point."
  (save-excursion
    (let* ((inhibit-read-only t))
      (when pos (goto-char pos))
      (goto-char (line-beginning-position))
      (delete-char 1)
      (insert (apply #'propertize "*"
                     (append (list 'face 'dired-mark)
                             (text-properties-at (line-beginning-position)))))
      (consult-gh--dired-goto-file-name-position)
      (add-text-properties (point) (line-end-position) (list ' face 'dired-marked)))))

(defun consult-gh--dired-unmark-line (&optional pos)
  "Unmark the line at POS.

POS defaults to current point."
  (save-excursion
    (let* ((inhibit-read-only t)
           (file-path (get-text-property (point) :path))
           (file-mode (get-text-property (point) :mode))
           (file-name (if (stringp file-path)
                          (pcase file-mode
                            ("directory"
                             (file-name-as-directory file-path))
                            (_
                             (file-name-nondirectory file-path)))
                        ""))
           (face (consult-gh-dired--get-face file-name file-mode)))

      (when pos (goto-char pos))
      (when (consult-gh-dired-line-marked-p)
        (goto-char (line-beginning-position))
        (delete-char 1)
        (insert (apply #'propertize " " (append (text-properties-at (line-beginning-position))
                                                (list 'face face))))
        (consult-gh--dired-goto-file-name-position)
        (add-text-properties (point) (line-end-position) (list 'face face))))))

(defun consult-gh--dired-toggle-mark (&optional pos)
  "Toggle mark on the line at POS.

marked line becomes unmarked and unmakrd line becomes marked.
POS defaults to current point."
    (if (consult-gh-dired-line-marked-p pos)
        (consult-gh--dired-unmark-line pos)
      (consult-gh--dired-mark-line pos)))

(defun consult-gh--dired-fold-show-directories ()
  "Show headers of directories only in `consult-gh-dired-mode'."
  (save-excursion
  (goto-char (point-min))
  (consult-gh--dired-goto-next-directory-header)
  (let ((inhibit-read-only t))
    (while (and (< (point) (point-max))
                (equal (forward-line +1) 0))
      (unless (equal (get-text-property (line-beginning-position) :mode) "directory")
        (put-text-property (max (- (line-beginning-position) 1) (point-min)) (line-end-position) 'invisible 'consult-g-dired))))))

(defun consult-gh--dired-fold-show-root-only ()
"Show only root dir header and directories in `consult-gh-dired-mode'."
  (save-excursion
    (consult-gh--dired-unhide (point-min) (point-max))
    (goto-char (point-min))
    (while (and (< (point) (point-max))
                (consult-gh--dired-goto-next-directory-header))
      (consult-gh-dired-hide-subdir))))

(defun consult-gh--dired-fold-show-all ()
  "Show all files and directories in `consult-gh-dired-mode'."
  (save-excursion
    (if (text-property-any (point-min) (point-max) 'invisible 'consult-gh-dired)
	         (consult-gh--dired-unhide (point-min) (point-max)))))

(defun consult-gh--dired-next-line (arg)
  "Move forward ARG lines."
  (let ((line-move-visual)
        (goal-column))
    (line-move arg t)))

(defun consult-gh--dired-undo ()
  "Undo for `consult-gh-dired-mode'."
  (let ((inhibit-read-only t))
    (undo)))

(defun consult-gh-dired--get-icon (file mode)
  "Get the icon for FILE of MODE.

MODE is one of “directory”, “file”, “symlink”, or “commit”."
  (if (stringp file)
      (pcase mode
        ("directory"
         (cond
          ((functionp consult-gh-dired-dir-icon)
           (funcall consult-gh-dired-dir-icon (substring-no-properties file)))
          ((stringp consult-gh-dired-dir-icon)
           consult-gh-dired-dir-icon)
          (t " ")))
        ("file"
         (cond
          ((functionp consult-gh-dired-file-icon)
           (funcall consult-gh-dired-file-icon (substring-no-properties file)))
          ((stringp consult-gh-dired-file-icon)
           consult-gh-dired-file-icon)
          (t " ")))
        ("symlink"
         (cond
          ((functionp consult-gh-dired-symlink-icon)
           (funcall consult-gh-dired-symlink-icon (substring-no-properties file)))
          ((stringp consult-gh-dired-symlink-icon)
           consult-gh-dired-symlink-icon)
          (t " ")))
        ("commit"
         (cond
          ((functionp consult-gh-dired-commit-icon)
           (funcall consult-gh-dired-commit-icon (substring-no-properties file)))
          ((stringp consult-gh-dired-commit-icon)
           consult-gh-dired-commit-icon)
          (t " "))))
    " "))

(defun consult-gh-dired--get-face (file mode)
  "Get the face for FILE of MODE.

MODE is one of “directory”, “file”, “symlink”, or “commit”."
  (if (stringp file)
      (pcase mode
        ("directory"
         (cond
          ((functionp consult-gh-dired-dir-face)
           (funcall consult-gh-dired-dir-face (substring-no-properties file)))
          ((type-of consult-gh-dired-dir-face)
           consult-gh-dired-dir-face)
          (t 'default)))
        ("file"
         (cond
          ((functionp consult-gh-dired-file-face)
           (funcall consult-gh-dired-file-face (substring-no-properties file)))
          ((symbolp consult-gh-dired-file-face)
           consult-gh-dired-file-face)
          (t 'default)))
        ("symlink"
         (cond
          ((functionp consult-gh-dired-symlink-face)
           (funcall consult-gh-dired-symlink-face (substring-no-properties file)))
          ((symbolp consult-gh-dired-symlink-face)
           consult-gh-dired-symlink-face)
          (t "")))
        ("commit"
         (cond
          ((functionp consult-gh-dired-commit-face)
           (funcall consult-gh-dired-commit-face (substring-no-properties file)))
          ((symbolp consult-gh-dired-commit-face)
           consult-gh-dired-commit-face)
          (t 'default))))
    'default))

(defun consult-gh-dired--format (file)
  "Format FILE for `consult-gh-dired'.

FILE must be a propertized string for example a string
in the list retrieved from `consult-gh--files-list-items'."
  (when (and (consp file)
             (plistp (cdr file)))
    (let* ((file-repo (plist-get (cdr file) :repo))
           (file-ref (plist-get (cdr file) :ref))
           (file-path (plist-get (cdr file) :path))
           (file-api-url (plist-get (cdr file) :api-url))
           (file-type (plist-get (cdr file) :object-type))
           (file-size (plist-get (cdr file) :size))
           (file-sha (plist-get (cdr file) :sha))
           (file-mode (plist-get (cdr file) :mode))
           (file-mode (pcase file-mode
                        ("100644" "file")
                        ("100755" "exec file")
                        ("040000" "directory")
                        ("160000" "commit")
                        ("120000" "symlink")
                        (_ file-mode)))
           (file-name (if (stringp file-path)
                          (pcase file-mode
                            ("directory"
                             (file-name-as-directory file-path))
                            (_
                             (file-name-nondirectory file-path)))
                        ""))
           (_ (if (and (stringp file-name) (length> file-name 0))
                  (add-text-properties 0 1 (list :consult-gh-dired-file-name-begin t) file-name)))
           (file-parent (when (stringp file-path)
                          (pcase file-mode
                            ("directory"
                             (file-name-directory (file-name-as-directory file-path)))
                            (_
                             (file-name-directory file-path)))))
           (icon (consult-gh-dired--get-icon file-name file-mode))
           (face (consult-gh-dired--get-face file-name file-mode))
           (str  (pcase file-mode
                   ("directory"
                    (propertize (concat
                                 "  "
                                 (if file-parent
                                     (make-string (+ (cl-count ?/ file-parent) 2)  ?\s))
                                 icon
                                 " "
                                 (if (equal file-parent "./") nil (propertize "./" 'face face :consult-gh-dired-file-name-begin t))
                                 (propertize file-name 'face face)
                                 ":")
                                :class "file"
                                :type "file"
                                :object-type file-type
                                :mode file-mode
                                :path file-path
                                :size file-size
                                :parent file-parent
                                'help-echo "mouse-2: visit this directory in other window"
                                'mouse-face 'highlight))
                   (_
                    (propertize (concat
                                 "  "
                                 (if file-parent (make-string (+ (cl-count ?/ file-parent) 2)  ?\s))
                                 icon
                                 " "
                                 (if file-parent nil (propertize "./" 'face face :consult-gh-dired-file-name-begin t))
                                 (propertize file-name 'face face)
                                 (cond
                                  ((equal file-mode "file")
                                   (if file-size
                                       (propertize
                                        (concat "\t"
                                                (file-size-human-readable file-size))
                                        'face 'consult-gh-visibility
                                        'consult-gh-dired-details t)))
                                  ((equal file-mode "symlink")
                                   (propertize
                                    (concat "  -> "
                                            (or (consult-gh--files-get-content-by-api-url file-api-url)
                                                ""))
                                    'face face
                                    'consult-gh-dired-details t))
                                  ((equal file-mode "commit")
                                   (if (stringp file-sha)
                                       (propertize (concat "  -> "
                                                           (propertize (concat (or
                                                                                (consult-gh--json-to-hashtable (consult-gh--api-get-command-string (concat (format "repos/%s/contents/%s" file-repo file-path)
                                                                                                                                                           (if file-ref (format "?ref=%s" file-ref))))
                                                                                                               :submodule_git_url)
                                                                                "")
                                                                               "@"
                                                                               (substring file-sha 0 6))
                                                                       'face 'link))
                                                   'consult-gh-dired-details t)))))
                                 :class "file"
                                 :type "file"
                                 :object-type file-type
                                 :path file-path
                                 :mode file-mode
                                 :parent file-parent
                                 :size file-size
                                 'help-echo "mouse-2: visit this file in other window"
                                 'mouse-face 'highlight)))))
           str)))

(defun consult-gh-dired--save-file (repo path mode save-path &optional ref)
"Save the file at path in `consult-gh-dired-mode'.

This is used to save files when in `consult-gh-dired-mode'.

Description of Arguments:
  REPO       a string; full name of a repository.
  PATH       a string; path of the file to copy.
  MODE       a string; mode of object “file”, “directory”, “symlink”
             or “commit”.
  SAVE-PATH  a string; the path of the folder where the files should
             be saved in.
  REF        a string; the name of a branch or tag in REPO."
  (pcase mode
        ("directory"
         (let* ((targetdir (or consult-gh-default-save-directory default-directory))
                (dir-path (and (stringp path)
                               (file-name-as-directory path)))
                (files-list (consult-gh--files-nodirectory-items repo dir-path ref nil "30s"))
                (save-path (or save-path (consult-gh--read-local-file nil "Save in Directory: " (and (stringp targetdir) (file-name-as-directory targetdir)) nil nil))))
           (when (listp files-list)
             (mapc (lambda (f)
                     (when (and (consp f)
                                (plistp (cdr f)))
                       (let* ((info (cdr f))
                              (file-path (plist-get info :path))
                              (save-path (expand-file-name file-path save-path)))
                         (when (and file-path save-path)
                          (consult-gh--files-save-file repo file-path save-path ref)))))
                   files-list))))
        ("file"
         (let* ((targetdir (or consult-gh-default-save-directory default-directory))
                (filename (and (stringp path) (file-name-nondirectory path)))
                (save-path (or save-path (file-truename (consult-gh--read-local-file nil "Enter File Save Path: " (expand-file-name filename (and (stringp targetdir) (file-name-as-directory targetdir))) nil nil))))
                (save-path (if (and (stringp save-path)
                                    (file-directory-p save-path))
                               (expand-file-name filename save-path)
                             save-path)))
           (consult-gh--files-save-file repo path save-path ref)))
        (_ (message "%s" (format "consult-gh: cannot save %s on local disk because it is a %s" (propertize path 'face 'consult-gh-warning)  (propertize mode 'face 'consult-gh-warning)))
                                   nil)))

(defun consult-gh-dired--find-file (repo path mode &optional ref no-select tempdir find-func)
"Open the file at path in `consult-gh-dired-mode'.

This is used to visit files when in `consult-gh-dired-mode'.

Description of Arguments:
  PATH      a string; path of the file to copy.
  REPO      a string; full name of a repository.
  REF       a string; the name of a branch or tag in REPO.
  MODE      a string; mode of object “file”, “directory”, “symlink”
            or “commit”.
  NO-SELECT a boolean; when non-nil do not switch to buffer.
  TEMPDIR   a string; the directory where the temporary file is saved
  FIND-FUNC a function; function to use instead of `find-file'."
(let* ((api-url (and repo path
                     (concat (format "repos/%s/contents/%s" repo path) (if ref (format "?ref=%s" ref)))))
       (info (if api-url (consult-gh--api-get-command-string api-url)))
       (git-url (if info (consult-gh--json-to-hashtable info :git_url))))

(cond
 ((and (or (equal mode "file")  (equal mode "symlink")) (stringp path))
  (if no-select
              (consult-gh--files-view repo path git-url t tempdir nil ref)
            (consult-gh--files-view repo path git-url nil tempdir nil ref nil find-func)))
 ((and (equal mode "commit") (stringp path))
  (let* ((type (consult-gh--json-to-hashtable info :type)))
    (if (equal type "submodule")
        (let* ((git-url (consult-gh--json-to-hashtable info :submodule_git_url))
               (module-sha (consult-gh--json-to-hashtable info :sha))
               (module-sha (and (stringp module-sha) (propertize module-sha :type "sha")))
               (urlparsed (url-generic-parse-url git-url))
               (urlhost (and urlparsed (url-host urlparsed)))
               (urlpath (car-safe (and urlparsed (url-path-and-query urlparsed)))))
          (cond
           ((stringp urlhost)
            (cond
             ((string-match-p ".*github.*" urlhost)
              (consult-gh-dired (string-remove-suffix ".git" (string-remove-prefix "/" urlpath)) nil module-sha))
             (t
              (and (y-or-n-p (format "Cannot open that submodule in consult-gh.  Do you want to browse the link:  %s? " git-url))
                      (funcall (or consult-gh-browse-url-func #'browse-url) git-url)))))
                  ((and (stringp urlpath)
                        (string-match-p "git@.*" urlpath))
                   (cond
                    ((string-match "git@.*github.com:\\(?1:.*\\)" urlpath)
                    (consult-gh-dired (string-remove-prefix "/" (string-remove-suffix ".git" (match-string 1 urlpath))) nil module-sha))
                    ((string-match "git@.*gitlab.com:\\(?1:.*\\)" urlpath)
                     (let ((submodule-link (format "https://gitlab.com/%s" (match-string 1 urlpath))))
                        (and (y-or-n-p (format "Cannot open that submodule in consult-gh.  Do you want to open the link:  %s in the browser? " (propertize submodule-link 'face 'consult-gh-warning)))
                    (funcall (or consult-gh-browse-url-func #'browse-url) submodule-link))))
                    (t
                      (message "Do not know how to open the submodule: %s" git-url)))))))))
         ((equal mode "directory")
          (if no-select
              nil
            (consult-gh-dired repo (or (and (stringp path)
                                            (file-name-as-directory path))
                                       path)
                              ref (and consult-gh-files-reuse-dired-like-buffer
(current-buffer)))))
         (t
          (message "No file on this line")))))

;;; Frontend interactive commands

;;;###autoload
(defun consult-gh-auth-switch (&optional host user prompt)
  "Switch between authenticated accounts.

If the optional arguments, HOST and USER are non-nil, use them for
authenticaiton otherwise query the user to select an account.
If PROMPT is non-nil, use it as the query prompt."
  (interactive "P")
  (unless (and host user)
    (let* ((prompt (or prompt "Select Account:"))
           (accounts (consult-gh--auth-accounts))
           (sel (consult--read accounts
                               :prompt prompt
                               :lookup #'consult--lookup-cons
                               :sort nil
                               :annotate (lambda (cand)
                                           (let* ((info (assoc cand accounts))
                                                  (host (cadr info))
                                                  (status (if (caddr info) "active" ""))
                                                  (current (if (equal info (or consult-gh--auth-current-account (consult-gh--auth-current-active-account))) "selected" "")))
                                             (format "\t\t%s\s\s%s\s\s%s"
                                                     (propertize host 'face 'consult-gh-tags)
                                                     (propertize status 'face 'consult-gh-user)
                                                     (propertize current 'face 'consult-gh-visibility)))))))
      (when (and sel (consp sel))
        (setq user (car sel))
        (setq host (cadr sel)))))
  (consult-gh--auth-switch host user))

(defun consult-gh--repo-list-transform (input)
  "Add annotation to repo candidates in `consult-gh-repo-list'.

Format each candidates with `consult-gh--repo-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--repo-format cand input nil))))

(defun consult-gh--repo-list-builder (input)
  "Build gh command line for listing repos of INPUT.

INPUT must be a GitHub user or org as a string e.g. “armindarvish”."

  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-repo-list-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-repo-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--repo-list (org)
  "List repos of ORG synchronously.

This runs the command  “gh repo list ORG”
using `consult-gh--command-to-string' to get a list of all repositories
of ORG, and returns the results in a list.

Each candidate is formatted by `consult-gh--repo-format'.

ORG must be the name of a github account as a string e.g. “armindarvish”."
  (let* ((maxnum (format "%s" consult-gh-repo-maxnum))
         (repolist  (or (consult-gh--command-to-string "repo" "list" org "--limit" maxnum) ""))
         (repos (split-string repolist "\n")))
    (mapcar (lambda (src) (consult-gh--repo-format src org nil))  (remove "" repos))))

(defun consult-gh--async-repo-list (prompt builder &optional initial min-input)
  "List repos of GitHub users/organizations asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-repo-list'.

It runs the command `consult-gh--repo-list-builder' in an async process
and returns the results \(list of repos of a user\) as a completion table
that will be passed to `consult--read'.  The completion table gets
dynamically updated as the user types in the minibuffer.
Each candidate in the minibuffer is formatted by
`consult-gh--repo-list-transform' to add annotation and other info to
the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (let* ((current-repo (consult-gh--get-repo-from-directory))
         (current-user (consult-gh--get-current-username))
         (current-user-orgs (consult-gh--get-current-user-orgs))
         (initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't) (consult-gh--get-username current-repo)))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read
                           (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--repo-list-transform)
                             :min-input min-input)
                           :prompt prompt
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--repo-state)
                           :initial initial
                           :group #'consult-gh--repo-group
                           :add-history  (mapcar (lambda (item) (when item (concat  (consult-gh--get-split-style-character) item)))
                                                 (append (list current-user)
                                                         (when (listp current-user-orgs)
                                                           current-user-orgs)
                                                         (list
                                                          (when current-repo
                                                            (consult-gh--get-username current-repo))
                                                          (thing-at-point 'symbol))
                                                         consult-gh--known-orgs-list))
                           :history '(:input consult-gh--orgs-history)
                           :require-match t
                           :category 'consult-gh-repos
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-repo-list (&optional initial noaction prompt min-input)
  "Interactive command to list repos of users/organizations asynchronously.

This is an interactive wrapper function around `consult-gh--async-repo-list'.
It queries the user to enter the name of a GitHub organization/username
in the minibuffer, then fetches a list of repositories for the entered
username and present them as a minibuffer completion table for selection.
The list of candidates in the completion table are dynamically updated
as the user changes the entry.

Upon selection of a candidate:
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to `consult-gh-repo-action'.

Additional command line arguments can be passed in the minibuffer entry
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
armindarvish -- -L 100
the async process will run the command “gh repo list armindarvish -L 100”,
which sets the limit for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-orgs-list' for quick access
in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer.
\(passed as INITITAL to `consult-gh--async-repo-list'\).

When PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-repo-list'.

For more details on consult--async functionalities,
see `consult-grep' and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq initial (or initial (consult-gh--get-username (substring-no-properties (get-text-property 0 :repo  (consult-gh-search-repos initial t)))))))
  (let* ((prompt (or prompt "Enter User or Org Name:  "))
         (sel (consult-gh--async-repo-list prompt #'consult-gh--repo-list-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-repo-action sel))))

(defun consult-gh--search-repos-transform (input)
  "Add annotation to repo candidates in `consult-gh-search-repos'.

Format each candidates with `consult-gh--repo-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--repo-format cand input t))))

(defun consult-gh--search-repos-builder (input)
  "Build gh command line for searching repositories with INPUT query.

The command arguments such as \(e.g. “gh search repos INPUT”\)."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-search-repos-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-repo-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-search-repos (prompt builder &optional initial min-input)
  "Seacrhes GitHub repositories asynchronously.

This is a non-interactive internal function.  For the interactive version
see `consult-gh-search-repos'.

It runs the command line from `consult-gh--search-repos-builder' in an
async process and returns the results \(list of search results for the
minibuffer input\) as a completion table in minibuffer that will be passed
to `consult--read'.  The completion table gets dynamically updated as the
user types in the minibuffer.  Each candidate in the minibuffer is
formatted by `consult-gh--search-repos-transform' to add annotation and
other info to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'
  INITIAL   an optional arg for the initial input in the minibuffer \(passed
            as INITITAL to `consult--read'\).
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (let* ((initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't) (consult-gh--get-repo-from-directory) nil))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read
                           (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--search-repos-transform)
                             :min-input min-input)
                           :prompt prompt
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--repo-state)
                           :initial initial
                           :group #'consult-gh--repo-group
                           :add-history  (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                                (localrepo (consult-gh--get-repo-from-directory)))
                                           (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                                                 (append (list
                                                          topicrepo
                                                          localrepo
                                                          " -- --owner @me"
                                                          (thing-at-point 'symbol))
                                                         consult-gh--known-orgs-list)))
                           :history '(:input consult-gh--search-repos-history)
                           :require-match t
                           :category 'consult-gh-repos
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-search-repos (&optional initial noaction prompt min-input)
  "Interactively search GitHub repositories.

This is an interactive wrapper function around
`consult-gh--async-search-repos'.  It queries the user to enter the name of
a GitHub organization/username in the minibuffer, then fetches a list
of repositories for the entered username.  The list of candidates in the
completion table are dynamically updated as the user changes the input.

Upon selection of a candidate either
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to `consult-gh-repo-action'

Additional commandline arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
consult-gh -- -L 100
and the async process will run “gh search repos -L 100”,
which sets the limit for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-orgs-list' for quick access
in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer.
\(passed as INITITAL to `consult-gh--async-repo-list'\).

When PROMPT is non-nil, use it as the query prompt.

For more details on consult--async functionalities,
see `consult-grep' and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (let* ((prompt (or prompt "Search Repos:  "))
         (host (consult-gh--auth-account-host))
         (sel (if (stringp host)
                  (with-environment-variables
                      (("GH_HOST" (or host consult-gh-default-host)))
                    (consult-gh--async-search-repos prompt #'consult-gh--search-repos-builder initial min-input))
                (consult-gh--async-search-repos prompt #'consult-gh--search-repos-builder initial min-input))))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (progn
        (funcall consult-gh-repo-action sel)
        sel))))

(defun consult-gh-orgs (&optional orgs noaction prompt)
  "List repositories of ORGS.

This is a wrapper function around `consult-gh--repo-list'.
If ORGS is nil, this simply calls `consult-gh--repo-list'.
If ORGS is a list, then it runs `consult-gh--repo-list' on every member
of ORGS and returns the results \(repositories of all ORGS\).

If NOACTION is non-nil, return the candidate without running action.
If PROMPT is non-nil, use it as the query prompt."
  (if (not orgs)
      (consult-gh-repo-list nil noaction))
  (let* ((prompt (or prompt "Select Repo: "))
         (candidates (consult--slow-operation "Collecting Repos ..." (apply #'append (mapcar (lambda (org) (consult-gh--repo-list org)) orgs))))
         (sel (consult-gh-with-host (consult-gh--auth-account-host)
                                    (consult--read candidates
                                                   :prompt prompt
                                                   :lookup #'consult--lookup-member
                                                   :state (funcall #'consult-gh--repo-state)
                                                   :group #'consult-gh--repo-group
                                                   :add-history (mapcar (lambda (item) (concat (consult-gh--get-split-style-character) item))
                                                                         (append (list (consult-gh--get-repo-from-directory)
(consult-gh--get-repo-from-topic)
(thing-at-point 'symbol))
                                                                        consult-gh--known-repos-list))
                                                   :history t
                                                   :require-match t
                                                   :category 'consult-gh-repos
                                                   :preview-key consult-gh-preview-key
                                                   :sort t))))

     (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
       (add-to-history 'consult-gh--repos-history (concat (consult-gh--get-split-style-character) reponame))
       (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--orgs-history (concat (consult-gh--get-split-style-character) username))
      (add-to-history 'consult-gh--known-orgs-list username))

    (if noaction
        sel
      (funcall consult-gh-repo-action sel))))

;;;###autoload
(defun consult-gh-user-repos (&optional user noaction)
  "List all repos for USER.

This includes repos of orgs of USER.  It uses
`consult-gh--get-current-user-orgs' to get all
orgs of USER.

If NOACTION is non-nil, return the candidate without running action."
  (interactive)
  (if user
      (consult-gh-orgs (consult-gh--get-current-user-orgs user t) noaction)
    (consult-gh-orgs (consult-gh--get-current-user-orgs nil t) noaction)))

;;;###autoload
(defun consult-gh-favorite-repos ()
  "List repositories of orgs in `consult-gh-favorite-orgs-list'.

Passes `consult-gh-favorite-orgs-list' to `consult-gh-orgs',
a useful command for quickly fetching a list of personal GitHub Repositories
or any other favorite accounts whose repositories are frequently visited."
  (interactive)
  (consult-gh-orgs consult-gh-favorite-orgs-list))

(define-obsolete-function-alias 'consult-gh-default-repos #'consult-gh-favorite-repos "2.0")

;;;###autoload
(defun consult-gh-repo-fork (&optional repos)
  "Interactively fork REPOS.

It runs the command “gh fork repo ...” to fork a repository
using the internal function `consult-gh--repo-fork'

If REPOS not supplied, interactively asks user to pick REPOS."
  (interactive)
  (let* ((repos (or repos (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos nil t))))))
    (if (stringp repos)
        (setq repos (list repos)))
    (mapcar (lambda (repo)
              (let* ((package (consult-gh--get-package repo))
                     (name (if consult-gh-confirm-name-before-fork (read-string (concat "name for " (propertize (format "%s: " repo) 'face 'consult-gh-repo)) package) package)))
                (consult-gh-with-host (consult-gh--auth-account-host) (consult-gh--repo-fork repo name))))
            repos)))

;;;###autoload
(defun consult-gh-repo-clone (&optional repos targetdir extra-args)
  "Interactively clone REPOS to TARGETDIR.

It runs the command “gh clone repo ...” to clone a repository
using the internal function `consult-gh--repo-clone'.

If REPOS or TARGETDIR are not supplied, interactively asks user
to pick them.

EXTRA-ARGS are passed to “git clone”.  With prefix arg, user is qeried to
edit/enter EXTRA-ARGS."
  (interactive)
  (let* ((repos (or repos (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos nil t)))))
         (targetdir (or targetdir consult-gh-default-clone-directory default-directory))
         (extra-args (cond
                      ((listp extra-args)
                         extra-args)
                      ((stringp extra-args)
                       (list extra-args))))
         (extra-args (or (and current-prefix-arg
                              (consult--read extra-args
                                             :prompt "Extra args for git (e.g., --depth 1 --branch develop): "
                                             :require-match nil
                                             :sort nil
                                             :history 'consult-gh--repo-clone-extra-args-history))
                         extra-args))
         (clonedir (if consult-gh-confirm-before-clone
                       (read-directory-name "Select Target Directory: " (and (stringp targetdir) (file-name-as-directory targetdir)))
                     (and (stringp targetdir) (file-name-as-directory targetdir))))
         (extra-args (if (and (stringp extra-args)
                              (not (string-empty-p extra-args)))
                         (consult--split-escaped (concat "-- " extra-args)))))
    (if (stringp repos)
        (setq repos (list repos)))
    (mapcar (lambda (repo)
              (let* ((package (consult-gh--get-package repo))
                     (name (if consult-gh-confirm-before-clone (read-string (concat "name for " (propertize (format "%s: " repo) 'face 'consult-gh-repo)) package) package)))
                (consult-gh-with-host (consult-gh--auth-account-host)
                                      (consult-gh--repo-clone repo name clonedir extra-args))))
            repos)))

(defun consult-gh-repo-create (&optional name)
 "Interactively create repository with NAME.

This is a wrapper around the internal function `consult-gh--repo-create'.

If name is nil, interatively ask the user for the name."

  (interactive "P")
  (consult-gh--repo-create name))

;;;###autoload
(defun consult-gh-repo-delete (&optional repo noconfirm)
  "Interactively delete REPO.

It runs the command “gh repo delete ...” to delete a repository
using the internal function `consult-gh--repo-delete'.

If REPOS are not supplied, interactively asks user to pick them from
`consult-gh-user-repos'.

When NOCONFIRM is non-nil, do not ask for confirmation"
  (interactive)
  (when-let* ((repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-user-repos nil t))))))
    (consult-gh-with-host
     (consult-gh--auth-account-host)
     (consult-gh--repo-delete repo (or noconfirm (not consult-gh-confirm-before-delete-repo))))))

;;;###autoload
(defun consult-gh-repo-rename (&optional repo new-name noconfirm)
  "Interactively rename REPO to NEW-NAME.

It runs the command “gh repo rename --repo REPO ...” to rename a
repository using the internal function `consult-gh--repo-rename'.

If REPOS are not supplied, interactively asks user to pick them from
`consult-gh-user-repos'.

When NOCONFIRM is non-nil, do not ask for confirmation"
  (interactive)
  (when-let* ((repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-user-repos nil t))))))
    (consult-gh-with-host
     (consult-gh--auth-account-host)
     (consult-gh--repo-rename repo new-name (or noconfirm (not consult-gh-confirm-before-rename-repo))))))

;;;###autoload
(defun consult-gh-repo-edit-settings (&optional repo)
  "Edit the REPO.

REPO must be a propertized text describing a repo similar to one
returned by `consult-gh-repo-list'.

This mimicks the same interactive repo editing from “gh repo edit” in
the terminal.
For more details refer to the manual with “gh repo edit --help”."
  (interactive "P")

  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-repo-view-mode)
       (let* ((repo (or (and repo (get-text-property 0 :repo repo))
                        (and consult-gh--topic (get-text-property 0 :repo consult-gh--topic))
                        (get-text-property 0 :repo (consult-gh-user-repos nil t))))
              (cand (propertize repo :repo repo))
              (canadmin (consult-gh--user-canadmin repo))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account)))))
         (if (not canadmin)
             (message "The current user, %s, %s to edit this repo" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))
           (funcall #'consult-gh--repo-view-action cand)
           (consult-gh-repo-edit-settings)))
     (let* ((topic consult-gh--topic)
            (repo (get-text-property 0 :repo topic))
            (canadmin (consult-gh--user-canadmin repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (if (not canadmin)
                   (message "The current user, %s, %s to edit this repo" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
            (owner (substring-no-properties (consult-gh--get-username repo)))
            (name (substring-no-properties (consult-gh--get-package repo)))
            (info (consult-gh--command-to-string "repo" "view" repo "--json" "description,visibility,repositoryTopics,hasDiscussionsEnabled,hasIssuesEnabled,hasProjectsEnabled,hasWikiEnabled,squashMergeAllowed,defaultBranchRef,deleteBranchOnMerge,isBlankIssuesEnabled,mergeCommitAllowed,rebaseMergeAllowed,isTemplate,homepageUrl"))
            (description (consult-gh--json-to-hashtable info :description))
            (visibility (consult-gh--json-to-hashtable info :visibility))
            (repo-topics (consult-gh--json-to-hashtable info :repositoryTopics))
            (repo-topics (and repo-topics
                              (listp repo-topics)
                              (mapcar (lambda (item) (gethash :name item)) repo-topics)))
            (discussions (equal (consult-gh--json-to-hashtable info :hasDiscussionsEnabled) t))
            (issues (equal (consult-gh--json-to-hashtable info :hasIssuesEnabled) t))
            (projects (equal (consult-gh--json-to-hashtable info :hasProjectsEnabled) t))
            (wiki (equal (consult-gh--json-to-hashtable info :hasWikiEnabled) t))
            (squash (equal (consult-gh--json-to-hashtable info :squashMergeAllowed) t))
            (default-branch (consult-gh--json-to-hashtable info :defaultBranchRef))
            (default-branch (and (hash-table-p default-branch)
                                 (gethash :name default-branch)))
            (delete-on-merge (equal (consult-gh--json-to-hashtable info :deleteBranchOnMerge) t))
            (blank-issues (equal (consult-gh--json-to-hashtable info :isBlankIssuesEnabled) t))
            (merge-commit (equal (consult-gh--json-to-hashtable info :mergeCommitAllowed) t))
            (rebase-merge (equal (consult-gh--json-to-hashtable info :rebaseMergeAllowed) t))
            (template (equal (consult-gh--json-to-hashtable info :isTemplate) t))
            (homepage-url (consult-gh--json-to-hashtable info :homepageUrl))
            (view-buffer (current-buffer))
            (buffer (format "*consult-gh-repo-edit: %s" repo))
            (newtopic (format "%s" repo))
            (type "repo"))

       (consult-gh--completion-set-repo-topics newtopic)

       (with-timeout (1 nil)
         (while (not (plist-member (text-properties-at 0 topic) :popular-topics))
           (sit-for 0.01)))

       (add-text-properties 0 1 (text-properties-at 0 topic) newtopic)
       (add-text-properties 0 1 (list :isComment nil :type type :new nil :name name :owner owner :original-description description :original-visibility visibility :original-homepage-url homepage-url :original-repo-topics repo-topics :original-default-branch default-branch :original-isTemplate template :original-issuesEnabled issues :original-blankIssuesAllowed blank-issues :original-discussionsEnabled discussions :original-wikiEnabled wiki :original-projectsEnabled projects :original-squashMergeAllowed squash :original-rebaseMergeAllowed rebase-merge :original-mergeCommitAllowed merge-commit :original-deleteOnMerge delete-on-merge
:description description :visibility visibility :homepage-url homepage-url :repo-topics repo-topics :default-branch default-branch :isTemplate template :issuesEnabled issues :blankIssuesAllowed blank-issues :discussionsEnabled discussions :wikiEnabled wiki :projectsEnabled projects :squashMergeAllowed squash :rebaseMergeAllowed rebase-merge :mergeCommitAllowed merge-commit :deleteOnMerge delete-on-merge :view-buffer view-buffer)
                            newtopic)

       ;; insert buffer contents
       (consult-gh-topics--insert-repo-contents (consult-gh-topics--get-buffer-create buffer "repo" newtopic) newtopic :name name :owner owner :repoTopics repo-topics :defaultBranch default-branch :description description :visibility visibility :homepageUrl homepage-url :isTemplate template :issuesEnabled issues :discussionsEnabled discussions :wikiEnabled wiki :projectsEnabled projects :squashMergeAllowed squash :rebaseMergeAllowed rebase-merge :mergeCommitAllowed merge-commit :deleteOnMerge delete-on-merge)

       (funcall consult-gh-pop-to-buffer-func buffer)))))

;;;###autoload
(defun consult-gh-repo-edit-readme (&optional repo)
  "Edit the README of REPO.

REPO must be a propertized text describing a repo similar to one
returned by `consult-gh-repo-list'."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-repo-view-mode)
       (let* ((repo (or (and repo (get-text-property 0 :repo repo))
                        (and consult-gh--topic (get-text-property 0 :repo consult-gh--topic))
                        (get-text-property 0 :repo (consult-gh-search-repos nil t))))
              (repo (and (stringp repo) (substring-no-properties repo)))
              (cand (and (stringp repo) (propertize repo :repo repo)))
              (canadmin (consult-gh--user-canadmin repo))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account)))))
         (if (not canadmin)
             (message "The current user, %s, %s to edit readme of that repo" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))
           (when cand
             (with-current-buffer (funcall #'consult-gh--repo-view-action cand)
             (consult-gh-repo-edit-readme)))))
     (consult-gh-repo--edit-readme))))

(defun consult-gh--issue-list-transform (input)
"Add annotation to issue candidates in `consult-gh-issue-list'.

Format each candidates with `consult-gh--issue-list-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--issue-list-format cand input nil))))

(defun consult-gh--issue-list-builder (input)
  "Build gh command line for listing issues the INPUT repository.

INPUT must be the full name of a GitHub repository as a string
e.g. “armindarvish/consult-gh”."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-issue-list-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-issue-maxnum)))))
    (unless (or (member "-s" flags) (member "--state" flags))
      (setq opts (append opts (list "--state" (format "%s" consult-gh-issues-state-to-show)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-issue-list (prompt builder &optional initial min-input)
  "List issues GitHub repos asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-issue-list'.

This runs the command line from `consult-gh--repo-list-builder'
in an async process and returns the results \(list of issues
for a repository\) as a completion table in minibuffer.  The completion
table gets dynamically updated as the user types in the minibuffer to
change the entry.
Each candidate in the minibuffer is formatted by
`consult-gh--issue-list-transform' to add annotation and other info
to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (let* ((initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't)
                          (consult-gh--get-repo-from-directory)
                        nil))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--issue-list-transform)
                             :min-input min-input)
                           :prompt prompt
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--issue-state)
                           :initial initial
                           :group #'consult-gh--issue-group
                           :require-match t
                           :category 'consult-gh-issues
                           :add-history  (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                                (localrepo (consult-gh--get-repo-from-directory)))
                                           (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                                                 (append (list (when topicrepo topicrepo)
                                                               (when localrepo localrepo)
                                                               (when localrepo (concat localrepo " -- --assignee @me"))
                                                               (when localrepo (concat localrepo " -- --author @me"))
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                           :history '(:input consult-gh--repos-history)
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-issue-list (&optional initial noaction prompt min-input)
  "Interactively list issues of a GitHub repository.

This is an interactive wrapper function around `consult-gh--async-issue-list'.
With prefix ARG, first search for a repo using `consult-gh-search-repos',
then list issues of that selected repo with `consult-gh--async-issue-list'.

It queries the user to enter the full name of a GitHub repository in the
minibuffer \(expected format is “OWNER/REPO”\), then fetches the list of
issues of that repository and present them as a minibuffer completion
table for selection.  The list of candidates in the completion table are
dynamically updated as the user changes the minibuffer input.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned.
 - if NOACTION is nil     candidate is passed to `consult-gh-issue-action'.

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
armindarvish/consult-gh -- -L 100
and the async process will run
“gh issue list --repo armindarvish/consult-gh -L 100”, which sets the limit
for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-repos-list' for quick
access in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer.
\(passed as INITITAL to `consult-gh--async-issue-list'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-issue-list'

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'"
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq initial (or initial (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos initial t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
      (setq consult-gh--last-command #'consult-gh-issue-list))
  (let* ((prompt (or prompt "Enter Repo Name:  "))
        (sel (consult-gh--async-issue-list prompt #'consult-gh--issue-list-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-issue-action sel))))

(defun consult-gh--search-issues-transform (input)
  "Add annotation to issue candidates in `consult-gh-search-issues'.

Format each candidates with `consult-gh--search-issues-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--search-issues-format cand input nil))))

(defun consult-gh--search-issues-builder (input)
  "Build gh command line for searching issues of INPUT query."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-search-issues-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-issue-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-search-issues (prompt builder &optional initial min-input)
  "Search GitHub issues asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-search-issues'.

This runs the command line from `consult-gh--search-issues-builder' in
an async process and returns the results \(list of search results
for the input\) as a completion table in minibuffer.  The completion table
gets dynamically updated as the user types in the minibuffer.
Each candidate is formatted by `consult-gh--search-issues-transform'
to add annotation and other info to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer.
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (consult-gh-with-host (consult-gh--auth-account-host)
      (consult--read
       (consult--process-collection builder
         :transform (consult--async-transform-by-input #'consult-gh--search-issues-transform)
         :min-input min-input)
       :prompt prompt
       :lookup #'consult--lookup-member
       :state (funcall #'consult-gh--issue-state)
       :initial initial
       :group #'consult-gh--issue-group
       :require-match t
       :add-history (let* ((topicrepo (consult-gh--get-repo-from-topic))
                           (localrepo (consult-gh--get-repo-from-directory)))
                      (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                             (append (list (when topicrepo topicrepo)
                                           (when localrepo localrepo)
                                           (when localrepo (concat localrepo " -- --assignee @me"))
                                           (when localrepo (concat localrepo " -- --author @me"))
                                  (thing-at-point 'symbol))
                            consult-gh--known-repos-list)))
       :history '(:input consult-gh--search-issues-history)
       :category 'consult-gh-issues
       :preview-key consult-gh-preview-key
       :sort nil)))

;;;###autoload
(defun consult-gh-search-issues (&optional initial repo noaction prompt min-input)
  "Interactively search GitHub issues of REPO.

This is an interactive wrapper function around
`consult-gh--async-search-issues'.  With prefix ARG, first search for a
repo using `consult-gh-search-repos', then search issues of only that
selected repo.

It queries the user for a search term in the minibuffer, then fetches the
list of possible GitHub issue for the entered query and presents them as a
minibuffer completion table for selection.  The list of candidates in the
completion table are dynamically updated as the user changes the entry.

Upon selection of a candidate either
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to `consult-gh-issue-action'

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
consult-gh -- -L 100
and the async process will run “gh search issues consult-gh -L 100”,
which sets the limit for the maximum number of results to 100.

INITIAL is an optional arg for the initial input in the minibuffer
\(passed as INITITAL to `consult-gh--async-repo-list'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-search-issues'.

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (if current-prefix-arg
      (setq repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-search-issues))
  (let* ((prompt (or prompt "Search Issues:  "))
         (consult-gh-args (if repo (append consult-gh-args `("--repo " ,(format "%s" repo))) consult-gh-args))
         (sel (consult-gh--async-search-issues prompt #'consult-gh--search-issues-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-issue-action sel))))

(defun consult-gh-issue-view-comments (&optional issue)
  "View all comments of ISSUE."
  (interactive "P" consult-gh-issue-view-mode)
  (when consult-gh-issue-view-mode
  (let* ((issue (or issue consult-gh--topic))
         (inhibit-read-only t)
         (repo (get-text-property 0 :repo issue))
         (number (get-text-property 0 :number issue))
         (comments (consult-gh--issue-get-comments repo number))
         (comments-text (when (and comments (listp comments))
                           (consult-gh--issue-format-comments comments)))
         (regions (consult-gh--get-region-with-prop :consult-gh-issue-comments))
         (region (when (listp regions) (cons (caar regions) (cdar (last regions))))))
    (when comments-text
      (when region
        (goto-char (car region))
        (delete-region (car region) (cdr region))
        (delete-region (line-beginning-position) (line-end-position))
        (insert "\n"))
      (save-mark-and-excursion
        (insert (with-temp-buffer
                  (insert comments-text)
                  (consult-gh--format-view-buffer "issues")
                  (buffer-string))))
      (outline-hide-sublevels 1)))))

;;;###autoload
(defun consult-gh-issue-create (&optional repo title body)
  "Create a new issue with TITLE and BODY for REPO.

This mimicks the same interactive issue creation from “gh issue create” in
the terminal.
For more details refer to the manual with “gh issue create --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (issueEnabled (consult-gh--repo-has-issues-enabled-p repo))
          (_ (unless (eq issueEnabled 't)
               (error "Issue is not enabled for the repo %s" repo)))
          (canwrite (consult-gh--user-canwrite repo))
          (author (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (template (consult-gh--select-issue-template repo))
          (template-info (and (consp template) (plistp (cdr-safe template)) (cdr-safe template)))
          (template-name (and (consp template) (car-safe template)))
          (title (or title (and template-info (plist-get template-info :title))))
          (title (and title (stringp title) (not (string-empty-p title)) (propertize title :consult-gh-draft-title t 'rear-nonsticky t)))
          (body (or body (and template-info (plist-get template-info :body))))
          (body (and body (not (string-empty-p body)) (propertize body :consult-gh-draft-body t 'rear-nonsticky t)))
          (body (consult-gh--format-text-for-mode body))
          (assignees (and template-info (plist-get template-info :assignees)))
          (assignees (when assignees
                       (cond
                        ((listp assignees)
                         (mapconcat #'identity assignees ", "))
                        ((stringp assignees)
                         assignees))))
          (assignees (and assignees (stringp assignees) (not (string-empty-p assignees)) (propertize assignees  :consult-gh-draft-assignees t 'rear-nonsticky t)))
          (labels (and template-info (plist-get template-info :labels)))
          (labels (when labels
                    (cond
                     ((listp labels)
                      (mapconcat #'identity labels ", "))
                     ((stringp labels) labels))))
          (labels (and labels (stringp labels) (not (string-empty-p labels)) (propertize labels :consult-gh-draft-labels t 'rear-nonsticky t)))
          (buffer (format "*consult-gh-issue-create: %s" repo))
          (topic (or repo "new issue"))
          (type "issue"))
     ;; add properties to consult-gh--topic
     (add-text-properties 0 1 (list :number nil :type type :isComment nil :new t :repo (substring-no-properties repo) :author author :template template-name) topic)
     (consult-gh--completion-set-all-fields repo topic canwrite)

     ;; insert buffer contents
     (consult-gh-topics--insert-buffer-contents buffer topic :title title :body body :assignees assignees :labels labels :canwrite canwrite)

     ;; switch to buffer
     (funcall consult-gh-pop-to-buffer-func buffer))))

(defun consult-gh-issue-create-fill-body ()
  "Fill body of issue draft from template."
  (interactive nil consult-gh-topics-edit-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
  (let* ((issue consult-gh--topic)
         (type (get-text-property 0 :type issue))
         (new (get-text-property 0 :new issue)))
    (if (and (equal type "issue") new)
        (let* ((repo (get-text-property 0 :repo issue))
               (default-template (get-text-property 0 :template issue))
               (templates (consult-gh--get-issue-templates repo))
               (template-name (and templates (consult--read templates
                                                       :prompt "Select a template: "
                                                       :require-match nil
                                                       :sort t
                                                       :annotate (apply-partially (lambda (current cand)
                                                                                         (if (equal cand current) (propertize "\t <current template>" 'face 'consult-gh-warning)))
                                                                                  default-template))))
               (template (and template-name (cdr (assoc template-name templates))))
               (title (and template (plistp template) (plist-get template :title)))
               (title (and title (stringp title) (not (string-empty-p title)) title))
               (body (and template (plistp template) (plist-get template :body)))
               (body (and (stringp body) (not (string-empty-p body)) body))
               (assignees (and template (plistp template) (plist-get template :assignees)))
               (assignees (when assignees
                            (cond
                             ((listp assignees)
                              (consult-gh--list-to-string assignees))
                             ((and (stringp assignees) (not (string-empty-p assignees)))
                              assignees))))
               (labels (and template (plistp template) (plist-get template :labels)))
               (labels (when labels
                         (cond
                          ((listp labels)
                           (consult-gh--list-to-string labels))
                          ((and (stringp labels) (not (string-empty-p labels)))
                           labels))))
               (body-region (consult-gh--get-region-with-prop :consult-gh-draft-body))
               (body-beg (and body-region (car-safe (car-safe body-region))))
               (header-region (consult-gh--get-region-with-overlay :consult-gh-header))
               (header-end (and header-region (cdr-safe (car-safe (last header-region))))))
          (add-text-properties 0 1 (list :template template-name) issue)
          (goto-char (point-min))
          (when (re-search-forward "^.*title: " header-end t)
            (delete-region (point) (line-end-position))
          (when title
            (pcase major-mode
              ('gfm-mode (insert (propertize (string-trim title) :consult-gh-header t :consult-gh-draft-title t 'rear-nonsticky t)))
              ('markdown-mode (insert (propertize (string-trim title) :consult-gh-header t :consult-gh-draft-title t 'rear-nonsticky t)))
              ('org-mode (insert (propertize (string-trim
                                              (with-temp-buffer
                                                (insert title)
                                                (consult-gh--markdown-to-org)                                        (consult-gh--whole-buffer-string)))
                                             :consult-gh-header t :consult-gh-draft-title t 'rear-nonsticky t)))
              ('text-mode (insert (propertize (string-trim title) :consult-gh-header t :consult-gh-draft-title t 'rear-nonsticky t))))))

          (goto-char (point-min))
          (when (re-search-forward "^.*assignees: " header-end t)
            (delete-region (point) (line-end-position))
            (when assignees
              (pcase major-mode
                ('gfm-mode (insert (propertize (string-trim assignees) :consult-gh-header t :consult-gh-draft-assignees t 'rear-nonsticky t)))
                ('markdown-mode (insert (propertize (string-trim assignees) :consult-gh-header t :consult-gh-draft-assignees t 'rear-nonsticky t)))
                ('org-mode (insert (propertize (string-trim
                                                (with-temp-buffer
                                                  (insert assignees)
                                                  (consult-gh--markdown-to-org)                                        (consult-gh--whole-buffer-string)))
                                               :consult-gh-header t :consult-gh-draft-assignees t 'rear-nonsticky t)))
                ('text-mode (insert (propertize (string-trim assignees) :consult-gh-draft-assigees t 'rear-nonsticky t))))))


          (goto-char (point-min))
          (when (re-search-forward "^.*labels: " header-end t)
            (delete-region (point) (line-end-position))
            (when labels
              (pcase major-mode
                ('gfm-mode (insert (propertize (string-trim labels) :consult-gh-header t :consult-gh-draft-labels t 'rear-nonsticky t)))
                ('markdown-mode (insert (propertize (string-trim labels) :consult-gh-header t :consult-gh-draft-labels t 'rear-nonsticky t)))
                ('org-mode (insert (propertize (string-trim
                                                (with-temp-buffer
                                                  (insert labels)
                                                  (consult-gh--markdown-to-org)                                        (consult-gh--whole-buffer-string)))
                                               :consult-gh-header t :consult-gh-draft-labels t 'rear-nonsticky t)))
                ('text-mode (insert (propertize (string-trim assignees) :consult-gh-header t :consult-gh-draft-labels t 'rear-nonsticky t))))))


          (goto-char (or header-end body-beg (point-max)))
          (delete-region (point) (point-max))
          (when body
            (pcase major-mode
              ('gfm-mode (insert (propertize body :consult-gh-draft-body t 'rear-nonsticky t)))
              ('markdown-mode (insert (propertize body :consult-gh-draft-body t 'rear-nonsticky t)))
              ('org-mode (insert (propertize (with-temp-buffer
                                               (insert body)
                                               (consult-gh--markdown-to-org)                                        (consult-gh--whole-buffer-string))
                                             :consult-gh-draft-body t 'rear-nonsticky t)))
              ('text-mode (insert (propertize body :consult-gh-draft-body t 'rear-nonsticky t)))))

          (goto-char (point-min))
          (goto-char (line-end-position)))
      (message "not in a issue create draft buffer!")))))

;;;###autoload
(defun consult-gh-issue-edit (&optional issue)
  "Edit the ISSUE.

ISSUE must be a propertized text describing an issue similar to one
returned by `consult-gh-issue-list'.

This is wrappe around `consult-gh-issue--edit'.  It mimicks the same
interactive issue creation from “gh issue edit” in the terminal.
For more details refer to the manual with “gh issue edit --help”."
  (interactive "P")

  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-issue-view-mode)
       (let* ((repo (or (and issue (get-text-property 0 :repo issue))
                        (and consult-gh--topic (get-text-property 0 :repo consult-gh--topic))
                        (get-text-property 0 :repo (consult-gh-search-repos nil t))))
              (canwrite (consult-gh--user-canwrite repo))
              (sep (consult-gh--get-split-style-character))
              (issue (or issue
                          (and consult-gh--topic
                           (equal (get-text-property 0 :type consult-gh--topic) "issue")
                           consult-gh--topic)
                          (consult-gh-issue-list (if canwrite
                                                          repo
                                                        (concat repo " -- " "--author " "@me" sep))
                                                      t)))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
              (isAuthor (consult-gh--user-isauthor issue)))
         (if (not (or canwrite isAuthor))
             (message "The current user, %s, %s to edit this issue" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))
           (with-current-buffer
               (funcall #'consult-gh--issue-view-action issue)
             (consult-gh-issue-edit))))
     (consult-gh-issue--edit issue))))

;;;###autoload
(defun consult-gh-issue-close (&optional issue reason comment)
  "Close the ISSUE with an optional REASON and/or COMMENT.

This mimicks the same function as running “gh issue close” in the terminal.
For more details refer to the manual with “gh issue close --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((issue (or issue consult-gh--topic (consult-gh-issue-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--state " "open") t)))
          (repo (and (stringp issue) (get-text-property 0 :repo issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (state (and (stringp issue) (get-text-property 0 :state issue)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (_ (unless (and (equal type "issue") (equal state "OPEN"))
               (error "Can only close an OPEN issue.  Did not get one!")))
          (reason (or reason (consult--read (list (cons "Completed" "completed")
                                                  (cons "Not Planned" "not planned")
                                                  (cons "Do not add reason" ""))
                                            :prompt "Select Reason: "
                                            :lookup #'consult--lookup-cdr
                                            :require-match t)))
          (comment (or comment (consult--read nil
                                              :prompt "Comment: ")))
          (args (list "issue" "close" number "--repo" repo)))
     (when (equal type "issue")
       (setq args (append args
                          (and (stringp comment) (not (string-empty-p comment))
                               (list "--comment" comment))
                          (and (stringp reason) (not (string-empty-p reason))
                               (list "--reason" reason))))
       (consult-gh--make-process (format "consult-gh-issue-close-%s-%s" repo number)
              :when-done (lambda (_ str) (message str))
              :cmd-args args)))))

;;;###autoload
(defun consult-gh-issue-reopen (&optional issue comment)
  "Reopen the ISSUE with an optional COMMENT.

This mimicks the same function as running “gh issue reopen” in the terminal.
For more details refer to the manual with “gh issue reopen --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((issue (or issue consult-gh--topic (consult-gh-issue-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--state " "closed") t)))
          (repo (and (stringp issue) (get-text-property 0 :repo issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (state (and (stringp issue) (get-text-property 0 :state issue)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (_ (unless (and (equal type "issue") (equal state "CLOSED"))
               (error "Can only reopen a CLOSED issue.  Did not get one!")))
          (comment (or comment (consult--read nil
                                              :prompt "Comment: ")))
          (args (list "issue" "reopen" number "--repo" repo)))
     (setq args (append args
                        (and (stringp comment) (not (string-empty-p comment))
                             (list "--comment" comment))))
     (consult-gh--make-process (format "consult-gh-issue-reopen-%s-%s" repo number)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-issue-pin (&optional issue)
  "Pin the ISSUE.

This mimicks the same function as running “gh issue pin” in the terminal.
For more details refer to the manual with “gh issue pin --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((consult-gh-issue-list-args (list "issue" "list" "--json" "number,title,isPinned,labels,updatedAt,state" "--template" "\"{{range .}}{{if (not .isPinned)}}{{.number}}\t{{.state}}\t{{.title}}\t{{range .labels}}{{.name}}, {{end}}\t{{.updatedAt}}\n{{end}}{{end}}\"" "--repo"))
          (issue (or issue
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "issue")
                          consult-gh--topic)
                     (consult-gh-issue-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (and (stringp issue) (get-text-property 0 :repo issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (_ (unless (equal type "issue")
               (error "Can only pin an issue.  Did not get one!")))
          (args (list "issue" "pin" number "--repo" repo)))
     (consult-gh--make-process (format "consult-gh-issue-pin-%s-%s" repo number)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-issue-unpin (&optional issue)
  "Unpin the ISSUE.

This mimicks the same function as running “gh issue unpin” in the terminal.
For more details refer to the manual with “gh issue unpin --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((consult-gh-issue-list-args (list "issue" "list" "--json" "number,title,isPinned,labels,updatedAt,state" "--template" "\"{{range .}}{{if .isPinned}}{{.number}}\t{{.state}}\t{{.title}}\t{{range .labels}}{{.name}}, {{end}}\t{{.updatedAt}}\n{{end}}{{end}}\"" "--repo"))
          (issue (or issue
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "issue")
                          consult-gh--topic)
                     (consult-gh-issue-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (and (stringp issue) (get-text-property 0 :repo issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (_ (unless (equal type "issue")
               (and
                (error "Can only pin an issue.  Did not get one!"))))
          (args (list "issue" "unpin" number "--repo" repo)))
     (consult-gh--make-process (format "consult-gh-issue-unpin-%s-%s" repo number)
            :when-done (lambda (_ str) (message str))
            :cmd-args args))))

;;;###autoload
(defun consult-gh-issue-lock (&optional issue reason)
  "Lock the ISSUE with and optional REASON.

This mimicks the same function as running “gh issue lock” in the terminal.
For more details refer to the manual with “gh issue lock --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((issue (or issue
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "issue")
                          consult-gh--topic)
                     (consult-gh-issue-list
                      (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--search " "is:unlocked") t)))
          (repo (and (stringp issue) (get-text-property 0 :repo issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (_ (unless (equal type "issue")
               (error "Can only lock an issue.  Did not get one!")))
          (reason (or reason (consult--read (list (cons "Yay, it's Resolved." "resolved")
                                                  (cons "Off Topic, A!" "off_topic")
                                                  (cons "That's a Spam Bro!" "spam")
                                                  (cons "This is Too Heated, even for my taste!" "too_heated")
                                                  (cons "Do not add a reason" ""))
                                            :prompt "Select Reason: "
                                            :lookup #'consult--lookup-cdr
                                            :require-match t)))
          (args (list "issue" "lock" number "--repo" repo)))
     (setq args (append args
                        (and (stringp reason) (not (string-empty-p reason))
                             (list "--reason" reason))))
     (consult-gh--make-process (format "consult-gh-issue-lock-%s-%s" repo number)
                               :when-done (lambda (_ str) (if (and str (not (string-empty-p str))) (message str)
                                                             (message "%s in %s was %s!" (format "Issue %s" (propertize (concat "#" number) 'face 'consult-gh-issue)) (propertize repo 'face 'consult-gh-user) (propertize "locked" 'face 'consult-gh-error))))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-issue-unlock (&optional issue)
  "Unlock the ISSUE.

This mimicks the same function as running “gh issue unlock” in the terminal.
For more details refer to the manual with “gh issue unlock --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((issue (or issue consult-gh--topic (consult-gh-issue-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--search " "is:locked") t)))
          (repo (and (stringp issue) (get-text-property 0 :repo issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (_ (unless (equal type "issue")
               (error "Can only lock an issue.  Did not get one!")))
          (args (list "issue" "unlock" number "--repo" repo)))
     (consult-gh--make-process (format "consult-gh-issue-unlock-%s-%s" repo number)
                               :when-done (lambda (_ str) (if (and str (not (string-empty-p str))) (message str)
                                            (message "%s in %s was %s!" (format "Issue %s" (propertize (concat "#" number) 'face 'consult-gh-issue)) (propertize repo 'face 'consult-gh-user) (propertize "unlocked" 'face 'consult-gh-success))))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-issue-transfer (&optional issue target-repo)
  "Transfer the ISSUE to TARGET-REPO.

This mimicks the same function as running “gh issue transfer” in the terminal.
For more details refer to the manual with “gh issue transfer --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or (and (stringp issue) (get-text-property 0 :repo issue))
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (issueEnabled (consult-gh--repo-has-issues-enabled-p repo))
          (_ (unless (eq issueEnabled 't)
               (error "Issue is not enabled for the repo %s" repo)))
          (owner (consult-gh--get-username repo))
          (issue (or issue
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "issue")
                          consult-gh--topic)
                     (consult-gh-issue-list repo t)))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (_ (unless (equal type "issue")
               (error "Can only transfer an issue.  Did not get one!")))
          (target-repo (or target-repo
                           (get-text-property 0 :repo (consult-gh-repo-list owner t "Select Target Repository: "))))
          (new-issueEnabled (consult-gh--repo-has-issues-enabled-p target-repo))
          (target-repo (if (eq new-issueEnabled 't)
                           target-repo
                         (progn
                           (message "Issue is not enabled for the repo %s" target-repo)
                           (get-text-property 0 :repo (consult-gh-repo-list owner t "Select Another Target Repository: ")))))
          (args (list "issue" "transfer" number target-repo "--repo" repo )))
     (when (equal type "issue")
       (consult-gh--make-process "consult-gh-issue-close"
                                 :when-done (lambda (_ str) (message str))
                                 :cmd-args args)))))

;;;###autoload
(defun consult-gh-issue-delete (&optional issue no-confirm)
  "Delete the ISSUE.

When the optional argument, NO-CONFIRM, is non-nil, do not ask for
confirmation.
This mimicks the same function as running “gh issue delete” in the terminal.
For more details refer to the manual with “gh issue delete --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or (and (stringp issue) (get-text-property 0 :repo issue))
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (issueEnabled (consult-gh--repo-has-issues-enabled-p repo))
          (_ (unless (eq issueEnabled 't)
               (error "Issue is not enabled for the repo %s" repo)))
          (issue (or issue
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "issue")
                          consult-gh--topic)
                     (consult-gh-issue-list repo t)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (_ (unless (equal type "issue")
               (error "Can only transfer an issue.  Did not get one!")))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (args (list "issue" "delete" number "--repo" repo "--yes"))
          (count 1)
          (confirm (if no-confirm number
                     (consult--read nil :prompt (format "Type %s to confirm deletion: " number)))))
     (while (and (< count 5) (not (equal confirm number)))
       (cl-incf count)
       (setq confirm (consult--read nil :prompt (format "Type %s to confirm deletion (Try %s of 5): " number count))))
     (when (and (equal type "issue") (equal confirm number))
       (consult-gh--make-process "consult-gh-issue-close"
                                 :when-done (lambda (_ str) (message str))
                                 :cmd-args args)))))

;;;###autoload
(defun consult-gh-issue-develop (&optional issue base branch-repo)
  "Manage linked branches for an ISSUE.

The optional argument BASE is a base branch to develop off of.
The optional argument BRANCH-REPO is the repo's full name where a branch
should be created (if not in the same repo).

This mimicks the same function as running “gh issue develop” in the terminal.
For more details refer to the manual with “gh issue develop --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or (and (stringp issue) (get-text-property 0 :repo issue))
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (issueEnabled (consult-gh--repo-has-issues-enabled-p repo))
          (_ (unless (eq issueEnabled 't)
               (error "Issue is not enabled for the repo %s" repo)))
          (issue (or issue
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "issue")
                          consult-gh--topic)
                     (consult-gh-issue-list repo t)))
          (type (and (stringp issue) (get-text-property 0 :type issue)))
          (_ (unless (equal type "issue")
               (error "Can only use an issue.  Did not get one!")))
          (number (and (stringp issue) (get-text-property 0 :number issue)))
          (branches (consult-gh--command-to-string "issue" "develop" "--list" "--repo" repo number))
          (options (append (list (list "Make a New Branch" :new))
                           (mapcar (lambda (item)
                                     (let ((info (split-string item "\t" t)))
                                       (list (car info) (cadr info))))
                                   (split-string branches "[\n\r]" t))))
          (branch (consult--read options
                                 :prompt "Select or Create a Branch: "
                                 :lookup #'consult--lookup-cons
                                 :require-match t
                                 :sort nil))
          (branch-repo (or branch-repo (if (eq (cadr branch) :new)
                                           (get-text-property 0 :repo (consult-gh-search-repos repo t "Select the repo where you want to create the branch: "))
                                         repo)))
          (base (or base
                    (if (eq (cadr branch) :new)
                        (get-text-property 0 :ref
                                           (consult-gh--read-branch branch-repo nil (format "Select a reference branch in %s as starting point: " branch-repo) t nil)))))
          (args (if (eq (cadr branch) :new)
                    (list "issue" "develop" number "--repo" repo))))
     (cond
      ((and (equal type "issue") (eq (cadr branch) :new))
       (setq args (append args
                          (and (stringp branch-repo)
                               (not (string-empty-p branch-repo))
                               (not (equal branch-repo repo))
                               (list "--branch-repo" branch-repo))
                          (and (stringp base) (not (string-empty-p base))
                               (list "--base" base))))
       (consult-gh--make-process "consult-gh-issue-develop"
                                 :when-done (lambda (_ str) (message str))
                                 :cmd-args args))
      ((stringp (cadr branch))
       (with-current-buffer (consult-gh-find-file repo (car branch))
         (consult-gh-edit-file)))))))

(defun consult-gh--pr-list-transform (input)
  "Add annotation to issue candidates in `consult-gh-pr-list'.

Format each candidates with `consult-gh--pr-list-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--pr-list-format cand input nil))))

(defun consult-gh--pr-list-builder (input)
  "Build gh command line for listing pull requests of the INPUT repository.

INPUT must be the full name of a GitHub repository as a string
e.g. “armindarvish/consult-gh”."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-pr-list-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-issue-maxnum)))))
    (unless (or (member "-s" flags) (member "--state" flags))
      (setq opts (append opts (list "--state" (format "%s" consult-gh-prs-state-to-show)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-pr-list (prompt builder &optional initial min-input)
  "List pull requests of GitHub repos asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-pr-list'.

This runs the command line from `consult-gh--pr-list-builder'
in an async process and returns the results (list of prs for a repository)
as a completion table in minibuffer.  The completion table gets dynamically
updated as the user types in the minibuffer to change the input.
Each candidate in the minibuffer is formatted by
`consult-gh--pr-list-transform' to add annotation to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"

  (let* ((initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't)
                          (consult-gh--get-repo-from-directory)
                        nil))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read
                           (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--pr-list-transform)
                             :min-input min-input)
                           :prompt prompt
                           :category 'consult-gh-prs
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--pr-state)
                           :initial initial
                           :group #'consult-gh--pr-list-group
                           :require-match t
                           :add-history (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                               (localrepo (consult-gh--get-repo-from-directory)))
                                          (mapcar (lambda (item) (concat (consult-gh--get-split-style-character) item))
                                                 (append (list (when topicrepo topicrepo)
                                                               (when localrepo localrepo)
                                                               (when localrepo (concat localrepo " -- --assignee @me"))
                                                               (when localrepo (concat localrepo " -- --author @me"))
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                           :history '(:input consult-gh--repos-history)
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-pr-list (&optional initial noaction prompt min-input)
  "Interactively list pull requests of a GitHub repository.

This is an interactive wrapper function around `consult-gh--async-pr-list'.
With prefix ARG, first search for a repo using `consult-gh-search-repos',
then list prs of that selected repo with `consult-gh--async-pr-list'.

It queries the user to enter the full name of a GitHub repository in
the minibuffer (expected format is “OWNER/REPO”), then fetches the list
of pull requests for that repository and presents them as a minibuffer
completion table for selection.  The list of candidates in the completion
table are dynamically updated as the user changes the entry.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned
 - if NOACTION is nil     candidate is passed to `consult-gh-pr-action'

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
armindarvish/consult-gh -- -L 100
and the async process will run
“gh pr list --repo armindarvish/consult-gh -L 100”,
which sets the limit for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-repos-list' for quick access
in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer
\(passed as INITITAL to `consult-gh--async-issue-list'\).

If PROMPT is non-nil, use it as the query prompt.

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq initial (or initial (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos initial t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-pr-list))
  (let* ((initial (or initial
                      (and (stringp consult-gh--topic)
                           (get-text-property 0 :repo consult-gh--topic))))
         (prompt (or prompt "Enter Repo Name:  "))
         (sel (consult-gh--async-pr-list prompt #'consult-gh--pr-list-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-pr-action sel))))

(defun consult-gh--search-prs-transform (input)
  "Add annotation to pr candidates in `consult-gh-search-prs'.

Format each candidates with `consult-gh--search-prs-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--search-prs-format cand input nil))))

(defun consult-gh--search-prs-builder (input)
  "Build gh command line for searching pull requests of INPUT query."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-search-prs-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-issue-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-search-prs (prompt builder &optional initial min-input)
  "Search GitHub pull requests asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-search-prs'.

This runs the command line from `consult-gh--search-prs-builder' in
an async process and returns the results (list of search results for
the input) as a completion table in minibuffer.  The completion table gets
dynamically updated as the user types in the minibuffer.
Each candidate in the minibuffer is formatted by
`consult-gh--search-prs-transform' to add annotation to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer.
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (consult-gh-with-host (consult-gh--auth-account-host)
                        (consult--read
                         (consult--process-collection builder
                           :transform (consult--async-transform-by-input #'consult-gh--search-prs-transform)
                           :min-input min-input)
                         :prompt prompt
                         :category 'consult-gh-prs
                         :lookup #'consult--lookup-member
                         :state (funcall #'consult-gh--pr-state)
                         :initial initial
                         :group #'consult-gh--pr-search-group
                         :require-match t
                         :add-history (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                             (localrepo (consult-gh--get-repo-from-directory)))
                                          (mapcar (lambda (item) (concat (consult-gh--get-split-style-character) item))
                                                 (append (list (when topicrepo topicrepo)
                                                               (when localrepo localrepo)
                                                               (when localrepo (concat localrepo " -- --review-requestd @me"))
                                                               (when localrepo (concat localrepo " -- --assignee @me"))
                                                               (when localrepo (concat localrepo " -- --mentions @me"))
                                                               (when localrepo (concat localrepo " -- --author @me"))
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                         :history '(:input consult-gh--search-prs-history)
                         :preview-key consult-gh-preview-key
                         :sort nil)))

;;;###autoload
(defun consult-gh-search-prs (&optional initial repo noaction prompt min-input)
  "Interactively search GitHub pull requests of REPO.

This is an interactive wrapper function around
`consult-gh--async-search-prs'.  With prefix ARG, first search for a repo
using `consult-gh-search-repos', then search prs of only that selected repo.

It queries the user for a search term in the minibuffer, then fetches
the list of possible GitHub pr candidates for the entered query
and presents them as a minibuffer completion table for selection.
The list of candidates in the completion table are dynamically updated as
the user changes the input.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned
 - if NOACTION is nil     candidate is passed to `consult-gh-pr-action'

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
consult-gh -- -L 100
and the async process will run “gh search prs consult-gh -L 100”,
which sets the limit for the maximum number of results to 100.

INITIAL is an optional arg for the initial input in the minibuffer
\(passed as INITITAL to `consult-gh--async-repo-list'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-search-prs'

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (if current-prefix-arg
      (setq repo (or repo (substring-no-properties (get-text-property 0 :repo  (consult-gh-search-repos repo t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-search-prs))
  (let* ((prompt (or prompt "Search Pull-Requests:  "))
         (consult-gh-args (if repo (append consult-gh-args `("--repo " ,(format "%s" repo))) consult-gh-args))
         (sel (consult-gh--async-search-prs prompt #'consult-gh--search-prs-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-pr-action sel))))

(defun consult-gh-pr-view-diff (&optional pr)
  "View diff for PR."
  (interactive "P")
  (cond
   (consult-gh-topics-edit-mode
    (consult-gh-topics--pr-create-view-diff))
   (consult-gh-pr-view-mode
    (consult-gh--pr-view-diff-action (or pr consult-gh--topic)))))

(defun consult-gh-pr-view-commits (&optional pr)
  "Insert commits text of PR in the current buffer."
  (interactive "P")
  (cond
   (consult-gh-topics-edit-mode
    (consult-gh-topics--pr-create-view-commits))
   (consult-gh-pr-view-mode
    (let* ((pr (or pr consult-gh--topic))
           (inhibit-read-only t)
           (commits-text (consult-gh--pr-view-get-commits pr))
           (regions (consult-gh--get-region-with-prop :consult-gh-pr-commits))
           (region (when (listp regions) (cons (caar regions) (cdar (last regions))))))
    (when commits-text
      (when region
        (goto-char (car-safe region))
        (delete-region (car region) (cdr region)))
      (save-excursion
        (insert commits-text)))))))

(defun consult-gh-pr-view-file-changes (&optional pr)
  "Insert file change diff text of PR in current buffer."
  (interactive "P")
  (cond
   (consult-gh-topics-edit-mode
    (consult-gh-topics--pr-create-view-file-changes))
   (consult-gh-pr-view-mode
    (let* ((pr (or pr consult-gh--topic))
         (inhibit-read-only t)
         (diff-text (consult-gh--pr-view-get-file-changes pr))
         (regions (consult-gh--get-region-with-prop :consult-gh-pr-file-changes))
         (region (when (listp regions) (cons (caar regions) (cdar (last regions))))))
    (when diff-text
      (when region
        (goto-char (car-safe region))
        (delete-region (car region) (cdr region)))
      (save-excursion
        (insert diff-text)))))))

(defun consult-gh-pr-view-comments (&optional pr)
  "Insert file change diff text of PR in current buffer."
  (interactive "P" consult-gh-pr-view-mode)
  (when consult-gh-pr-view-mode
  (let* ((pr (or pr consult-gh--topic))
         (inhibit-read-only t)
         (repo (get-text-property 0 :repo pr))
         (number (get-text-property 0 :number pr))
         (comments (consult-gh--pr-get-comments repo number))
         (url (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/pull/%s" number)))
         (comments-text (when (and comments (listp comments))
                           (consult-gh--pr-format-comments comments repo number url)))
         (regions (consult-gh--get-region-with-prop :consult-gh-pr-comments))
         (region (when (listp regions) (cons (caar regions) (cdar (last regions))))))
    (when comments-text
      (when region
        (goto-char (car region))
        (delete-region (car region) (cdr region))
        (delete-region (line-beginning-position) (line-end-position))
        (insert "\n"))
      (save-excursion
        (insert (with-temp-buffer
                  (insert comments-text)
                  (consult-gh--format-view-buffer "pr")
                  (buffer-string)))
        (outline-hide-sublevels 2))))))

(defun consult-gh-pr-create (&optional repo title body basebranch headrepo headbranch)
  "Create a new pull request with TITLE and BODY for REPO.

This mimicks the same interactive pr creation from “gh pr create”
in the terminal.  For more details refer to the manual with
“gh pr create --help”.

Description of Arguments:
  REPO        a string; full name of the base (target) repository
  TITLE       a string; title of the pr
  BODY        a string; body of the pr
  BASEBRANCH  a string; name of the base ref branch
  HEADREPO    a string; name of the head (source) repository
  HEADBRANCH  a string; name of the head ref branch"
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t "Select the target base repo you want to merge to: "))))
          (isForked (equal (gethash :isFork (consult-gh--json-to-hashtable (consult-gh--command-to-string "repo" "view" repo "--json" "isFork"))) 't))
          (simrepos (consult-gh-topics--pr-get-similar repo))
          (selection (if isForked
                         (substring-no-properties (consult--read (append simrepos "Other")
                                                                 :prompt "That is a forked repo. Which one of the following repos you want to merge to?"
                                                                 :require-match t))))
          (baserepo (cond
                     ((equal selection "Other")
                      (get-text-property 0 :repo (consult-gh-search-repos (consult-gh--get-package repo) t "Search for the target base repo you want to merge to: ")))
                     ((stringp selection) selection)
                     (t repo)))
          (basebranch (or basebranch
                          (consult-gh--read-branch baserepo nil "Select the branch you want to merge to: " t nil)))
          (selection (unless headrepo
                       (cond
                        ((length> simrepos 0)
                         (consult--read (append (list (propertize baserepo 'face 'consult-gh-repo)) simrepos (list (propertize "Other" 'face 'consult-gh-date)))
                                        :prompt "Select the source head repo you want to merge from: "
                                        :require-match t
                                        :sort nil)))))
          (headrepo (or headrepo
                        (cond
                         ((equal selection "Other")
                          (get-text-property 0 :repo (consult-gh-search-repos (consult-gh--get-package baserepo) t "Search for the source head repo you want to merge from: ")))
                         ((stringp selection) selection)
                         (t baserepo))))
          (headbranch (or headbranch
                          (cond
                           ((equal baserepo headrepo)
                            (consult-gh--read-branch baserepo nil "Select the head branch: " t nil (lambda (cand) (not (equal (substring-no-properties cand) basebranch)))))
                           (t
                            (consult-gh--read-branch headrepo nil "Select the head branch: " t nil)))))
          (canwrite (consult-gh--user-canwrite baserepo))
          (author (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (title (and title
                      (stringp title)
                      (not (string-empty-p title))
                      (propertize title :consult-gh-draft-title t 'rear-nonsticky t)))
          (template (consult-gh--select-pr-template baserepo))
          (template-text (and template
                              (cdr-safe template)))
          (template-name (and template
                              (car-safe template)))
          (commits-body nil)
          (body (cond
                 ((and body (not (string-empty-p body)))
                  (propertize body :consult-gh-draft-body t 'rear-nonsticky t))
                 ((and template-text (stringp template-text) (not (string-empty-p template-text)))
                  (propertize template-text :consult-gh-template-body t 'rear-nonsticky t))
                 (t (and (if consult-gh-pr-create-confirm-fill
                             (y-or-n-p "Fill the body automatically from commits info?")
                           t)
                         (setq commits-body (consult-gh-topics--pr-body-text-from-commits baserepo basebranch headrepo headbranch))
                         (propertize commits-body :consult-gh-commits-body t 'rear-nonsticky t)))))
          (body (consult-gh--format-text-for-mode body))
          (topic (or repo "new pr"))
          (type "pr")
          (buffer (format "*consult-gh-pr-create: %s:%s<-%s:%s" baserepo basebranch headrepo headbranch)))

     ;; add properties to consult-gh--topic
     (add-text-properties 0 1 (list :number nil :type type :isComment nil :new t :repo (substring-no-properties baserepo) :author author :headrepo (substring-no-properties headrepo) :headbranch (substring-no-properties headbranch) :baserepo (substring-no-properties baserepo) :basebranch (substring-no-properties basebranch) :template template-name) topic)
     (consult-gh--completion-set-all-fields repo topic canwrite)
     ;; insert buffer contents
     (consult-gh-topics--insert-buffer-contents buffer topic :title title :body body :baserepo baserepo :basebranch basebranch :headrepo headrepo :headbranch headbranch :canwrite canwrite)
     ;; switch to buffer
     (funcall consult-gh-pop-to-buffer-func buffer))))

(defun consult-gh-pr-create-fill-body ()
  "Fill body of PR draft from commits or templates."
  (interactive nil consult-gh-topics-edit-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
  (let* ((pr consult-gh--topic)
         (type (get-text-property 0 :type pr))
         (new (get-text-property 0 :new pr)))
    (if (and (equal type "pr") new)
        (let* ((baserepo (get-text-property 0 :baserepo pr))
               (templates (consult-gh--get-pr-templates baserepo))
               (sources (append '("Commits") (if templates '("Templates"))))
               (source (consult--read sources
                                      :prompt "What do you want to use to fill the body?")))
               (pcase source
                       ("Templates" (consult-gh-topics--pr-fill-body-from-template))
                       ("Commits" (consult-gh-topics--pr-fill-body-from-commits))))))))

;;;###autoload

(defun consult-gh-pr-edit (&optional pr)
  "Edit the PR.

This mimicks the same function as running “gh pr edit” in the terminal.
For more details refer to the manual with “gh pr edit --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-pr-view-mode)
       (let* ((baserepo (or (and pr (or (get-text-property 0 :baserepo pr)
                                        (get-text-property 0 :repo pr)))
                            (and consult-gh--topic (or (get-text-property 0 :baserepo consult-gh--topic)
                                 (get-text-property 0 :repo consult-gh--topic)))
                            (get-text-property 0 :repo (consult-gh-search-repos nil t))))
              (canwrite (consult-gh--user-canwrite baserepo))
              (sep (consult-gh--get-split-style-character))
              (pr (or pr
                      (and consult-gh--topic
                           (equal (get-text-property 0 :type consult-gh--topic) "pr")
                           consult-gh--topic)
                      (consult-gh-pr-list (if canwrite
                                              baserepo
                                            (concat baserepo " -- " "--author " "@me" sep))
                                          t)))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
              (isAuthor (consult-gh--user-isauthor pr)))
         (if (not (or canwrite isAuthor))
             (message "The current user, %s, %s to edit this pull request" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))
           (with-current-buffer (funcall #'consult-gh--pr-view-action pr)
           (consult-gh-pr-edit))))
     (let* ((pr consult-gh--topic)
            (isAuthor (consult-gh--user-isauthor pr))
            (baserepo (get-text-property 0 :baserepo pr))
            (canwrite (consult-gh--user-canwrite baserepo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (if (not (or canwrite isAuthor))
                   (message "The current user, %s, %s to edit this pull request" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
            (number (get-text-property 0 :number pr))
            (newtopic (format "%s/#%s" baserepo number))
            (title (get-text-property 0 :title pr))
            (body (get-text-property 0 :body pr))
            (reviewers (get-text-property 0 :reviewers pr))
            (assignees (get-text-property 0 :assignees pr))
            (labels (get-text-property 0 :labels pr))
            (projects (get-text-property 0 :projects pr))
            (milestone (get-text-property 0 :milestone pr))
            (basebranch (get-text-property 0 :basebranch pr))
            (headrepo (get-text-property 0 :headrepo pr))
            (headbranch (get-text-property 0 :headbranch pr))
            (buffer (format "*consult-gh-pr-edit: %s #%s" baserepo number))
            (type "pr"))

       ;; collect valid refs for completion at point
       (consult-gh--completion-set-pr-refs newtopic baserepo nil t)

       (if canwrite
           ;; collect valid projects for completion at point
           (consult-gh--completion-set-valid-projects newtopic baserepo)
         (add-text-properties 0 1 (list :valid-projects nil) newtopic))

       (add-text-properties 0 1 (list :isComment nil :type type :new nil :original-title title :original-body body :original-reviewers reviewers :original-assignees assignees :original-labels labels :original-milestone milestone :original-projects projects :original-baserepo baserepo :original-basebranch basebranch :original-headrepo headrepo :original-headbranch headbranch) newtopic)


       (with-timeout (1 nil)
         (while (not (plist-member (text-properties-at 0 pr) :valid-labels))
           (sit-for 0.01)))
       (add-text-properties 0 1 (text-properties-at 0 pr) newtopic)

(consult-gh-topics--insert-buffer-contents (consult-gh-topics--get-buffer-create buffer "pull request" newtopic) newtopic :title title :body body :assignees assignees :labels labels :milestone milestone :projects projects :reviewers reviewers :basebranch basebranch :canwrite canwrite)

       (funcall consult-gh-pop-to-buffer-func buffer)))))

;;;###autoload
(defun consult-gh-pr-merge (&optional pr)
  "Merge the PR.

This mimicks the same function as running “gh pr merge” in the terminal.
For more details refer to the manual with “gh pr merge --help”.

PR is a propertized string describing a pull request.  For example, PR can
be the text stored in the buffer-local variable `consult-gh--topic' in a
buffer created by `consult-gh--pr-view'."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-pr-view-mode)
       (let* ((repo (or (and pr (get-text-property 0 :repo pr))
                        (get-text-property 0 :repo (consult-gh-search-repos nil t))))
              (canwrite (consult-gh--user-canwrite repo))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
              (_ (if (not canwrite)
                     (message "The user, %s, %s to merge PRs in that repository" (propertize user 'face 'consult-gh-user) (propertize "does not have permissions" 'face 'consult-gh-error))))
              (pr (or pr (consult-gh-pr-list repo t))))
         (funcall #'consult-gh--pr-view-action pr)
         (consult-gh-pr-merge))

     (let* ((pr (or pr consult-gh--topic))
            (repo (get-text-property 0 :repo pr))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (if (not canwrite)
                   (message "The user, %s, %s to merge PRs in that repository" (propertize user 'face 'consult-gh-user) (propertize "does not have permissions" 'face 'consult-gh-error))))
            (number (get-text-property 0 :number pr))
            (state (get-text-property 0 :state pr)))

       (cond
        ((not (equal state "OPEN"))
         (message "Pull request is already %s!" (propertize (downcase state) 'face 'consult-gh-error)))
        (t
         (let* ((auto-merge-state (consult-gh--json-to-hashtable (consult-gh--command-to-string "pr" "view" number "--repo" repo "--json" "autoMergeRequest") :autoMergeRequest))
                (action (consult--read (append (list (cons "Merge the pull request" :merge))
                                               (and (not auto-merge-state) (list (cons "Enable auto-merge (after requirements are met)." :auto)))
                                               (and auto-merge-state (list (cons "Disable auto-merge" :disable-auto))))
                                       :prompt "What do you want to do?"
                                       :lookup #'consult--lookup-cdr
                                       :require-match t
                                       :sort nil)))
           (pcase action
             (':merge
              (consult-gh-pr--merge-merge pr))
             (':auto
              (consult-gh-pr--merge-enable-automerge pr))
             (':disable-auto
              (consult-gh-pr--merge-disable-automerge pr))))))))))

;;;###autoload
(defun consult-gh-pr-review (&optional pr)
  "Interactively create a new review on pull request, PR.

PR is a string with properties that identify a github pull requests.
For an example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh-pr-create'.
PR defaults to `consult-gh--topic' and if that is nil also, the user is
asked to chose it interactively."
  (interactive "P" consult-gh-pr-view-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((topic (or pr consult-gh--topic))
          (newtopic (substring-no-properties topic))
          (repo (or (and (stringp topic) (get-text-property 0 :repo topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (type (or (and (stringp topic) (get-text-property 0 :type topic))
                    "pr"))
          (number (and (stringp topic) (get-text-property 0 :number topic)))
          (buffer (format "*consult-gh-pr-review: %s - %s #%s review" repo type number))
          (view-buffer (buffer-name (current-buffer))))
     (add-text-properties 0 1 (list :review-buffer buffer :view-buffer view-buffer :target "review") topic)
     (add-text-properties 0 1 (text-properties-at 0 topic) newtopic)
     (add-text-properties 0 1 (list :new t :isComment nil :type "review" :target "review") newtopic)
     (cond
      (topic
       (funcall consult-gh-pop-to-buffer-func (consult-gh-topics--get-buffer-create buffer "review" newtopic)))
      ((equal type "pr")
       (funcall #'consult-gh--pr-view-action (consult-gh-pr-list repo t))
       (consult-gh-pr-review))
      (t (message "Did not get a pull request to review!"))))))

;;;###autoload
(defun consult-gh-pr-close (&optional pr comment delete-branch)
  "Close the PR with an optional COMMENT.

If the optional argument DELETE-BRANCH is non-nil, deletes
the local and remote branch after close.

This mimicks the same function as running “gh pr close” in the terminal.
For more details refer to the manual with “gh pr close --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((pr (or pr consult-gh--topic (consult-gh-pr-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--state " "open") t)))
          (repo (and (stringp pr) (get-text-property 0 :repo pr)))
          (type (and (stringp pr) (get-text-property 0 :type pr)))
          (state (and (stringp pr) (get-text-property 0 :state pr)))
          (number (and (stringp pr) (get-text-property 0 :number pr)))
          (_ (unless (and (equal type "pr") (equal state "OPEN"))
               (error "Can only close an OPEN pr.  Did not get one!")))
          (comment (or comment (consult--read nil
                                              :prompt "Comment: ")))
          (delete-branch (or delete-branch (y-or-n-p "Do you want to delete the remote and local (i.e. in the current directory) branches of this pull request?")))
          (args (list "pr" "close" number "--repo" repo)))
     (when (equal type "pr")
       (setq args (append args
                          (and (stringp comment) (not (string-empty-p comment))
                               (list "--comment" comment))
                          (and delete-branch
                               (list "--delete-branch"))))
       (consult-gh--make-process (format "consult-gh-pr-close-%s-%s" repo number)
                                 :when-done (lambda (_ str) (message str))
                                 :cmd-args args)))))

;;;###autoload
(defun consult-gh-pr-reopen (&optional pr comment)
  "Reopen the PR with an optional COMMENT.

This mimicks the same function as running “gh pr reopen” in the terminal.
For more details refer to the manual with “gh pr reopen --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((pr (or pr consult-gh--topic (consult-gh-pr-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--state " "closed") t)))
          (repo (and (stringp pr) (get-text-property 0 :repo pr)))
          (type (and (stringp pr) (get-text-property 0 :type pr)))
          (state (and (stringp pr) (get-text-property 0 :state pr)))
          (number (and (stringp pr) (get-text-property 0 :number pr)))
          (_ (unless (and (equal type "pr") (equal state "CLOSED"))
               (error "Can only reopen a CLOSED pull request.  Did not get one!")))
          (comment (or comment (consult--read nil
                                              :prompt "Comment: ")))
          (args (list "pr" "reopen" number "--repo" repo)))
     (setq args (append args
                        (and (stringp comment) (not (string-empty-p comment))
                             (list "--comment" comment))))
     (consult-gh--make-process (format "consult-gh-pr-reopen-%s-%s" repo number)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-pr-lock (&optional pr reason)
  "Lock the PR with and optional REASON.

This mimicks the same function as running “gh pr lock” in the terminal.
For more details refer to the manual with “gh pr lock --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((pr (or pr consult-gh--topic (consult-gh-pr-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--search " "is:unlocked") t)))
          (repo (and (stringp pr) (get-text-property 0 :repo pr)))
          (type (and (stringp pr) (get-text-property 0 :type pr)))
          (number (and (stringp pr) (get-text-property 0 :number pr)))
          (_ (unless (equal type "pr")
               (error "Can only lock a pull request.  Did not get one!")))
          (reason (or reason (consult--read (list (cons "Yay, it's Resolved." "resolved")
                                                  (cons "Off Topic, A!" "off_topic")
                                                  (cons "That's a Spam Bro!" "spam")
                                                  (cons "This is Too Heated, even for my taste!" "too_heated")
                                                  (cons "Do not add a reason" ""))
                                            :prompt "Select Reason: "
                                            :lookup #'consult--lookup-cdr
                                            :require-match t)))
          (args (list "pr" "lock" number "--repo" repo)))
     (setq args (append args
                        (and (stringp reason) (not (string-empty-p reason))
                             (list "--reason" reason))))
     (consult-gh--make-process (format "consult-gh-pr-lock-%s-%s" repo number)
                               :when-done (lambda (_ str) (if (and str (not (string-empty-p str))) (message str)
                                            (message "%s in %s was %s!" (format "Pull request %s" (propertize (concat "#" number) 'face 'consult-gh-issue)) (propertize repo 'face 'consult-gh-user) (propertize "locked" 'face 'consult-gh-error))))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-pr-unlock (&optional pr)
  "Unlock the PR.

This mimicks the same function as running “gh pr unlock” in the terminal.
For more details refer to the manual with “gh pr unlock --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((pr (or pr consult-gh--topic (consult-gh-pr-list (concat (get-text-property 0 :repo (consult-gh-search-repos nil t)) " -- " "--search " "is:locked") t)))
          (repo (and (stringp pr) (get-text-property 0 :repo pr)))
          (type (and (stringp pr) (get-text-property 0 :type pr)))
          (number (and (stringp pr) (get-text-property 0 :number pr)))
          (_ (unless (equal type "pr")
               (error "Can only lock a pull request.  Did not get one!")))
          (args (list "pr" "unlock" number "--repo" repo)))
     (consult-gh--make-process (format "consult-gh-pr-unlock-%s-%s" repo number)
                               :when-done (lambda (_ str) (if (and str (not (string-empty-p str))) (message str)
                                            (message "%s in %s was %s!" (format "Pull request %s" (propertize (concat "#" number) 'face 'consult-gh-issue)) (propertize repo 'face 'consult-gh-user) (propertize "unlocked" 'face 'consult-gh-success))))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-pr-mark-ready (&optional pr)
  "Mark the PR as ready for review.

This mimicks the same function as running “gh pr ready” in the terminal.
For more details refer to the manual with “gh pr ready --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((pr (or pr consult-gh--topic (consult-gh-pr-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (and (stringp pr) (get-text-property 0 :repo pr)))
          (type (and (stringp pr) (get-text-property 0 :type pr)))
          (number (and (stringp pr) (get-text-property 0 :number pr)))
          (_ (unless (equal type "pr")
               (error "Can only mark a pull request.  Did not get one!")))
          (args (list "pr" "ready" number "--repo" repo)))
     (consult-gh--make-process (format "consult-gh-pr-ready-%s-%s" repo number)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args))))

;;;###autoload
(defun consult-gh-pr-mark-draft (&optional pr)
  "Mark the PR as draft.

This mimicks the same function as running “gh pr ready” with the switch
“--undo” in the terminal.  For more details refer to the manual with
“gh pr ready --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((pr (or pr consult-gh--topic (consult-gh-pr-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (and (stringp pr) (get-text-property 0 :repo pr)))
          (type (and (stringp pr) (get-text-property 0 :type pr)))
          (number (and (stringp pr) (get-text-property 0 :number pr)))
          (_ (unless (equal type "pr")
               (error "Can only mark a pull request.  Did not get one!")))
          (args (list "pr" "ready" number "--repo" repo "--undo")))
     (consult-gh--make-process (format "consult-gh-pr-draft-%s-%s" repo number)
                               :when-done (lambda (_ str) (message str))
                               :cmd-args args))))

(defun consult-gh--search-code-transform (input)
  "Add annotation to code candidates in `consult-gh-search-code'.

Format each candidates with `consult-gh--search-code-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--search-code-format cand input nil))))

(defun consult-gh--search-code-builder (input)
  "Build gh command line for searching code with INPUT query."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-search-code-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-code-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-search-code (prompt builder &optional initial min-input)
  "Seacrh GitHub codes asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-search-code'.

This runs the command line from `consult-gh--search-code-builder' in
an async process and returns the results (list of search results for
the input) as a completion table in minibuffer.  The completion table
gets dynamically updated as the user types in the minibuffer.
Each candidate in the minibuffer is formatted by
`consult-gh--search-code-transform' to add annotation to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer.
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (consult-gh-with-host (consult-gh--auth-account-host)
                        (consult--read
                         (consult--process-collection builder
                           :transform (consult--async-transform-by-input #'consult-gh--search-code-transform)
                           :min-input min-input)
                         :prompt prompt
                         :category 'consult-gh-codes
                         :lookup #'consult--lookup-member
                         :state (funcall #'consult-gh--code-state)
                         :initial initial
                         :group #'consult-gh--code-group
                         :require-match t
                         :add-history (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                             (localrepo (consult-gh--get-repo-from-directory)))
                                          (mapcar (lambda (item) (concat (consult-gh--get-split-style-character) item))
                                                 (append (list " -- --owner @me"
                                                               (when topicrepo (concat " -- --repo " topicrepo))
                                                               (when localrepo (concat " -- --repo " localrepo))
                                                               (when localrepo (concat " -- --repo " localrepo " --owner @me"))
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                         :history '(:input consult-gh--search-code-history)
                         :preview-key consult-gh-preview-key
                         :sort nil)))

;;;###autoload
(defun consult-gh-search-code (&optional initial repo noaction prompt min-input)
  "Interactively search GitHub codes.

This is an interactive wrapper function around
`consult-gh--async-search-code'.  With prefix ARG, first search for a repo
using `consult-gh-search-repos', then search for code only on that
selected repo.

If REPO is non-nil search for code in REPO.

It queries the user for a search term in the minibuffer, then fetches
the list of possible GitHub code candidates for the entered query and
presents them as a minibuffer completion table for selection.
The list of candidates in the completion table are dynamically updated
as the user changes the input.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned
 - if NOACTION is nil     candidate is passed to `consult-gh-pr-action'

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
react -- -L 100
and the async process will run “gh search code react -L 100”,
which sets the limit for the maximum number of results to 100.

INITIAL is an optional arg for the initial input in the minibuffer
\(passed as INITITAL to `consult-gh--async-search-code'\).

If PROMPT is non-nil, use it as the query prompt.

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (setq consult-gh--open-files-list nil
        consult-gh--current-tempdir (consult-gh--tempdir))
  (if current-prefix-arg
      (setq repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t))))))
  (let* ((prompt (or prompt "Search Code:  "))
         (consult-gh-args (if repo (append consult-gh-args `("--repo " ,(format "%s" repo))) consult-gh-args))
         (sel (consult-gh--async-search-code prompt #'consult-gh--search-code-builder initial min-input)))
    (setq consult-gh--open-files-list nil)
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-code-action sel))))

(defun consult-gh--files-read-file (repo &optional ref path initial prompt require-match allow-dirs extension ref-type)
  "Read a file in REPO.

This is a non-interactive internal function.
For the interactive version see `consult-gh-find-file'.

Description of Arguments:
  REPO          a string; repository's full name
                \(e.g., armindarvish/consult-gh\)
  REF           a propertized string; branch name, tag name or commit sha
                The ref should have a property :type to specify whether
                it is a “branch”, “tag”, or “sha”.
  PATH          a string; when non-nil search is done relative to PATH
  INITIAL       a string; used as initial input in searching files
                \(gets passed to `consult--read'\).
  PROMPT        a string; prompt to use when selecting a file.
                If PATH is non-nil, it gets appended to the end
                of prompt as well.
  REQUIRE-MATCH a boolean; whether to require match
                \(gets passed to `consult--read'\).
  ALLOW-DIRS    a boolean; whether to allow selecting directory paths
  EXTENSION     a string; the format of files to show
                \(e.g., “.ext” only shows files that end in “.ext”\)
  REF-TYPE      a symbol: either nil, \='branch, \='tag, or \='commit
                when non-nil limit the selection to this type"
  (let* ((repo (or repo
                   (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t)))))
         (ref (or ref (consult-gh--read-ref repo nil nil nil ref-type t nil t) "HEAD"))
         (ref-str  (if (and (stringp ref)
                            (equal (get-text-property 0 :type ref) "sha"))
                       (substring-no-properties ref 0 6)
                     (substring-no-properties ref)))
         (new-prompt (if (stringp prompt)
                         (concat prompt (if path (format " %s " path)))
                       (concat (format "Find File in %s" repo)
                               (if ref-str (format " [%s]" ref-str))
                               ":"
                               (if path (format " %s" path))
                               " ")))
         (files-list (consult-gh--files-list-items repo path ref nil "30s"))
         (all-files (cond
                     ((and path
                           (stringp path)
                           (file-name-directory path))
                        (append (list (list "../" :repo repo :ref ref :api-url nil :path (file-name-directory (string-remove-suffix "/" path)) :size nil :object-type "tree" :new nil :mode "040000"))
                                files-list))
                     ((and (not path)
                           consult-gh-files-use-dired-like-mode)
                      (append (list (list "./" :repo repo :ref ref :api-url nil :path nil :size nil :object-type "tree" :new nil :mode "040000"))
                                files-list))
                     (t files-list)))
         (candidates (remove nil (mapcar (lambda (item)
                                                     (when (consp item)
                                                       (consult-gh--file-format item)))
                                                   all-files)))
         (sel (consult-gh-with-host
               (consult-gh--auth-account-host)
               (consult--read candidates
                              :prompt new-prompt
                              :lookup #'consult-gh--file-lookup
                              :annotate (consult-gh--file-annotate)
                              :state (funcall #'consult-gh--file-state)
                              :require-match require-match
                              :sort nil
                              :history 'consult-gh--files-history
                              :add-history (let* ((localfile (buffer-file-name))
                                                  (topicfile (when (stringp consult-gh--topic)
                                                               (get-text-property 0 :path consult-gh--topic))))
                                             (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                                                     (append (list
                                                              (thing-at-point 'symbol)
                                                              (when  localfile
                                                                (file-name-nondirectory localfile))
                                                              topicfile))))
                              :category 'consult-gh-files
                              :preview-key consult-gh-preview-key
                              :initial initial
                              :predicate (if (stringp extension)
                                             (lambda (cand)
                                               (let ((path (get-text-property 0 :path cand)))
                                                 (if (stringp path)
                                                     (string-suffix-p extension path)))))))))

    (when sel
      (if (and (stringp sel)
               (not (string-empty-p sel))
               (listp all-files)
               (assoc (substring-no-properties sel) all-files))
          (add-text-properties 0 1 (list :existing t) sel)
        (add-text-properties 0 1 (list :existing nil) sel))

      (cond
       ((and (stringp sel)
             (not (string-empty-p sel))
             (not (equal (file-name-as-directory sel) sel))
             (equal (get-text-property 0 :object-type sel) nil))
        (setq sel (string-remove-prefix (consult-gh--get-split-style-character) sel))
        (add-text-properties 0 1 (list :repo repo
                                       :user (consult-gh--get-username repo)
                                       :package (consult-gh--get-package repo)
                                       :path (if (stringp path)
                                                 (concat (file-name-directory path)
                                                         (substring-no-properties sel))
                                               (substring-no-properties sel))
                                       :api-url nil
                                       :mode "file"
                                       :size nil
                                       :ref ref
                                       :new t
                                       :class "file"
                                       :type "file"
                                       :object-type "blob"
                                       :existing nil)
                             sel)

        ;;add org and repo to known lists
        (when-let* ((reponame (get-text-property 0 :repo sel)))
          (add-to-history 'consult-gh--known-repos-list reponame))
        (when-let* ((username (get-text-property 0 :user sel)))
          (add-to-history 'consult-gh--known-orgs-list username))
        sel)
       ((and (stringp sel)
             (not (string-empty-p sel))
             (equal (get-text-property 0 :new sel) nil)
             (equal (get-text-property 0 :object-type sel) "tree")
             (or (not allow-dirs)
                 (string-prefix-p "../" (substring-no-properties sel))))
        (consult-gh--files-read-file repo ref (if (get-text-property 0 :path sel)
                                              (file-name-as-directory (get-text-property 0 :path sel)))
                              nil prompt require-match allow-dirs extension ref-type))
       ((and (stringp sel)
             (not (string-empty-p sel))
             (equal (get-text-property 0 :new sel) nil)
             (or (and allow-dirs (equal (get-text-property 0 :object-type sel) "tree"))
                 (equal (get-text-property 0 :object-type sel) "blob")))
        ;;add org and repo to known lists
        (when-let* ((reponame (get-text-property 0 :repo sel)))
          (add-to-history 'consult-gh--known-repos-list reponame))
        (when-let* ((username (get-text-property 0 :user sel)))
          (add-to-history 'consult-gh--known-orgs-list username))
        sel)
       (t
         ;;add org and repo to known lists
        (when-let* ((reponame (get-text-property 0 :repo sel)))
          (add-to-history 'consult-gh--known-repos-list reponame))
        (when-let* ((username (get-text-property 0 :user sel)))
          (add-to-history 'consult-gh--known-orgs-list username))
        sel)))))

;;;###autoload
(defun consult-gh-find-file (&optional repo ref path noaction initial)
  "Interactively find files of REPO in REF.

Query the user for name of a REPO, expected format is “OWNER/REPO”
\(e.g., armindarvish/consult-gh\), and a REF in REPO.  Then presents
the file contents of the REPO and REF for selection.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned
 - if NOACTION is nil     candidate is passed to `consult-gh-file-action'

Description of Arguments:
  REPO          a string; repository's full name
                \(e.g., armindarvish/consult-gh\)
  REF           a string; branch name, tag name or commit sha
  PATH          a string; when non-nil search is done relative to PATH
  INITIAL       a string; used as initial input in searching files
                \(gets passed to `consult--read'\).
  NOACTION      a boolean; if non-nil, only retrun the file object
                and do not call `consult-gh-file-action'."
  (interactive)
  (setq consult-gh--open-files-list nil
        consult-gh--current-tempdir (consult-gh--tempdir))
  (let* ((repo (or repo
                   (unless current-prefix-arg
                     (and (stringp consult-gh--topic)
                          (get-text-property 0 :repo consult-gh--topic)))
                   (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t)))))
         (ref (or ref
                  (unless current-prefix-arg
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :ref consult-gh--topic)))))
         (sel (if consult-gh-files-use-dired-like-mode
                  (consult-gh--files-read-file repo ref path initial nil nil t)
                (consult-gh--files-read-file repo ref path initial))))
    (when sel
      ;;add org and repo to known lists
      (when-let* ((reponame (get-text-property 0 :repo sel)))
        (add-to-history 'consult-gh--known-repos-list reponame))
      (when-let* ((username (get-text-property 0 :user sel)))
        (add-to-history 'consult-gh--known-orgs-list username))
      (if noaction
          sel
        (funcall consult-gh-file-action sel)))))

;;;###autoload
(defun consult-gh-edit-file (&optional file commit-buffer)
  "Edit FILE and commit to GitHub.

FILE must be a string with properties describing a file similar to the
local variable `consult-gh--topic' in  a buffer created by
`consult-gh-find-file'.

When COMMIT-BUFFER is non-nil, use it for commiting the edits."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-file-view-mode)
       (let* ((repo (or (and file (get-text-property 0 :repo file))
                        (and consult-gh--topic (get-text-property 0 :repo consult-gh--topic))
                        (get-text-property 0 :repo (consult-gh-search-repos nil t))))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
              (canwrite (consult-gh--user-canwrite repo))
              (_ (unless canwrite
                   (user-error "The current user, %s, %s to edit this file" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
              (file (or file
                        (and consult-gh--topic
                             (equal (get-text-property 0 :type consult-gh--topic) "file")
                             consult-gh--topic)
                        (consult-gh--files-read-file repo nil nil nil nil t nil nil 'branch)))
              (ref (and (stringp file)
                        (get-text-property 0 :ref file))))

         (when (stringp ref)
           (cond
            ((equal (get-text-property 0 :type ref) "sha")
             (user-error "Cannot edit files in a commit ref!"))
            ((equal (get-text-property 0 :type ref) "tag")
             (user-error "Cannot edit files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))

         (when file
           (with-current-buffer (funcall #'consult-gh--files-view-action file)
             (add-text-properties 0 1 (list :commit-buffer commit-buffer) consult-gh--topic)
             (consult-gh-edit-file nil commit-buffer))))
     (let* ((file (or file consult-gh--topic))
            (repo (get-text-property 0 :repo file))
            (ref (get-text-property 0 :ref file))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (unless canwrite
                 (user-error "The current user, %s, %s to edit this file" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error)))))


       (when (and (stringp ref)
                  (equal (get-text-property 0 :type ref) "sha"))
         (user-error "Cannot edit files in a commit ref!"))

       (when buffer-read-only
         (read-only-mode -1))
       (add-text-properties 0 1 (list :commit-buffer commit-buffer) file)
       (cond
        ((not (buffer-modified-p))
         (message "Make changes and when you %s the file, %s.  You can also use %s to push saved changes to GitHub."
                  (propertize "save" 'face 'consult-gh-warning)
                  (propertize "you get prompted to push to GitHub" 'face 'consult-gh-success)
                  (propertize "`consult-gh-push-file'" 'face 'consult-gh-success)))
        (t
         (consult-gh-push-file)))))))

;;;###autoload
(defun consult-gh-push-file ()
"Make a commit to push FILE change to GitHub."
(interactive nil consult-gh-file-view-mode)
(consult-gh-with-host
 (consult-gh--auth-account-host)
 (if consult-gh-file-view-mode
      (consult-gh--files-edit-commit-changes)
   (user-error "Not in a consult-gh file buffer!"))))

;;;###autoload
(defun consult-gh-create-file (&optional file content)
  "Create FILE.

Create a new buffer to edit file.  When the buffer is saved, the user
is asked if they want to push the file to GitHub.

FILE is a string with properties that describe repo, ref,
and path of file.  For example see the buffer-local-variable
`consult-gh--topic' in a buffer created by `consult-gh-file-view'.

If CONTENT is non-nil insert it in the file buffer."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((file (or file
                    (consult-gh--files-read-file nil nil nil nil "File Name: " nil nil nil 'branch)))
          (repo (get-text-property 0 :repo file))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (unless canwrite
               (user-error "The current user, %s, %s to edit this file" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (ref (or (get-text-property 0 :ref file)
                   (substring-no-properties (consult-gh--read-branch repo nil nil nil t))
                   (or consult-gh-default-branch-to-load "HEAD")))
          (ref (cond
                ((or (equal ref "HEAD") (equal ref ""))
                 (consult-gh--repo-get-default-branch repo))
                ((and (stringp ref)
                      (not (string-empty-p ref))
                      (substring-no-properties ref)))))
          (path (and (stringp file)
                     (not (string-empty-p file))
                     (get-text-property 0 :path file)))
          (path (and (stringp path)
                     (not (string-empty-p path))
                     path))
          (existing (get-text-property 0 :existing file)))

     (when (stringp ref)
       (cond
        ((equal (get-text-property 0 :type ref) "sha")
         (user-error "Cannot create files in a commit ref"))
        ((equal (get-text-property 0 :type ref) "tag")
         (user-error "Cannot create files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))

     (cond
      ((and (stringp file)
            existing
            (equal (get-text-property 0 :object-type file) "tree"))
       (let* ((new-file (consult-gh--files-read-file repo ref (file-name-as-directory path) nil "File Name: " nil nil nil 'branch)))
         (consult-gh-create-file new-file)))
      ((and (stringp file)
            existing
            (equal (get-text-property 0 :object-type file) "blob"))
       (pcase (consult--read (list (cons "Choose a different path" :reselect)
                                   (cons "Edit the existing file" :edit))
                             :prompt "That file already exists.  What do you want to do?"
                             :lookup #'consult--lookup-cdr
                             :require-match t
                             :sort nil)
         (':reselect
          (let* ((new-file (consult-gh--files-read-file repo ref (file-name-as-directory (file-name-directory path)) nil "File Name: " nil nil nil 'branch)))
            (consult-gh-create-file new-file)))
         (':edit
          (consult-gh-edit-file file))))
      (t
       (and repo path
            (let* ((tempdir (expand-file-name (concat repo "/" (or ref "HEAD") "/")
                                              (or consult-gh--current-tempdir (consult-gh--tempdir))))
                   (commit-buffer (get-text-property 0 :commit-buffer file)))
              (if (and (bufferp commit-buffer)
                       (buffer-live-p commit-buffer))
                  (with-current-buffer commit-buffer
                    (let ((files (get-text-property 0 :files consult-gh--topic)))
                      (add-text-properties 0 1 (list :files (append (list file) files)) consult-gh--topic)
                      (funcall consult-gh-pop-to-buffer-func commit-buffer)))
                (consult-gh--files-create-buffer repo path ref content tempdir)))))))))

;;;###autoload
(defun consult-gh-delete-file (&optional file)
  "Delete FILE.

FILE is a string with properties that describes repo, ref, and the
path of each file to delete.  For example see the buffer-local-variable
`consult-gh--topic' in a buffer created by
`consult-gh-file-view'."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((file (or file
                    (and (stringp consult-gh--topic)
                         (equal (get-text-property 0 :type consult-gh--topic) "file")
                         consult-gh--topic)
                    (consult-gh--files-read-file nil nil nil nil "File Name: " t t nil 'branch)))
          (repo (and (stringp file) (get-text-property 0 :repo file)))
          (type (and (stringp file) (get-text-property 0 :object-type file)))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (user-error "The current user, %s, %s to edit this file" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (ref (or (get-text-property 0 :ref file)
                   (get-text-property 0 :ref (consult-gh--read-branch repo nil nil t nil))
                   (or consult-gh-default-branch-to-load "HEAD")))
          (ref (cond
                ((or (equal ref "HEAD") (equal ref ""))
                 (consult-gh--repo-get-default-branch repo))
                ((and (stringp ref)
                      (not (string-empty-p ref))
                      (substring-no-properties ref)))))
          (path (and (stringp file)
                     (not (string-empty-p file))
                     (get-text-property 0 :path file)))
          (path (and (stringp path)
                     (not (string-empty-p path))
                     path)))

     (when (stringp ref)
       (cond
        ((equal (get-text-property 0 :type ref) "sha")
         (user-error "Cannot delete files in a commit ref"))
        ((equal (get-text-property 0 :type ref) "tag")
         (user-error "Cannot delete files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))

     (cond
      ((equal type "tree")
       (pcase (consult--read (list (cons "Choose a different path" :reselect)
                                   (cons "Delete the entire directory" :delete))
                             :prompt "You slected a directory.  What do you want to do?"
                             :lookup #'consult--lookup-cdr
                             :require-match t
                             :sort nil)
         (':reselect
          (let* ((new-file (consult-gh--files-read-file repo ref (file-name-as-directory path) nil "File Name: " t t nil 'branch)))
            (consult-gh-delete-file new-file)))
         (':delete
          (when (and repo path ref)
            (add-text-properties 0 1 (list :path (substring-no-properties path) :type "directory") path)
               (consult-gh--files-delete repo (list path) ref)))))
      (t
       (and repo path ref
            (add-text-properties 0 1 (list :path (substring-no-properties path) :type "file") path)
            (consult-gh--files-delete repo (list path) ref)))))))

;;;###autoload
(defun consult-gh-upload-files (&optional files topic)
  "Upload FILES to github.

TOPIC is a string with properties that describes repo, ref,
and the path for uploading files.  For example see the
buffer-local-variable `consult-gh--topic' in a buffer created by
`consult-gh-file-view'."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((topic (or topic consult-gh--upload-topic (consult-gh--files-read-upload-targets)))
          (repo (or (and (stringp topic) (get-text-property 0 :repo topic))
                    (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos nil t)))))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (user-error "The current user, %s, %s to edit this file" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (ref (or (and (stringp topic) (get-text-property 0 :ref topic))
                   (substring-no-properties (consult-gh--read-branch repo nil nil nil t))
                   (or consult-gh-default-branch-to-load "HEAD")))
          (ref (cond
                ((or (equal ref "HEAD") (equal ref ""))
                 (consult-gh--repo-get-default-branch repo))
                ((and (stringp ref)
                      (not (string-empty-p ref))
                      ref))))
          (_ (when (stringp ref)
               (cond
                ((equal (get-text-property 0 :type ref) "sha")
                 (user-error "Cannot upload files to a commit ref"))
                ((equal (get-text-property 0 :type ref) "tag")
                 (user-error "Cannot upload files to a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'")))))
          (dir (or (and (stringp topic) (get-text-property 0 :dir topic))
                   (consult--read (mapcar #'consult-gh--file-format (append (list (list "." :repo repo :ref ref :api-url nil :path nil :size nil :object-type "tree" :new nil))
                                                                            (consult-gh--files-directory-items repo nil ref nil)))
                                  :prompt "Select Directory: "
                                  :lookup #'consult-gh--file-lookup)))
          (path (or (and (stringp topic) (get-text-property 0 :path topic))
                    (and (stringp dir)
                         (not (string-empty-p dir))
                         (if (get-text-property 0 :object-type dir)
                             (get-text-property 0 :path dir)
                           (string-remove-suffix "/" dir)))))
          (files (or files
                     (and (stringp topic) (get-text-property 0 :files topic)))))

     (cond
      ((derived-mode-p 'dired-mode)
       (let* ((newfiles (consult-gh--files-upload-marked-dired-files))
              (newtopic (or topic (format "%s/%s/%s" repo ref path))))
         (add-text-properties 0 1 (list :title nil :repo repo :ref ref :path path :dir dir :files newfiles :dired-buffer (current-buffer)) newtopic)
         (setq-local consult-gh--upload-topic newtopic)
         (add-to-list 'consult-gh--upload-targets newtopic)
         (consult-gh--upload-commit newfiles repo path ref)))
      (t
       (let* ((newtopic (or topic (and (stringp topic) topic) (format "%s/%s/%s" repo ref path))))
         (add-text-properties 0 1 (list :title nil :repo repo :ref ref :path path :dir dir :files files) newtopic)
         (consult-gh--files-upload-dired newtopic files)))))))

;;;###autoload
(defun consult-gh-rename-file (&optional file)
  "Rename FILE.

FILE is a string with properties that describes repo, ref, and the
path of the file to delete.  For example see the buffer-local-variable
`consult-gh--topic' in a buffer created by
`consult-gh-file-view'."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((file (or file
                    (and (stringp consult-gh--topic)
                         (equal (get-text-property 0 :type consult-gh--topic) "file")
                         consult-gh--topic)
                    (consult-gh--files-read-file nil nil nil nil "File Name: " t t nil 'branch)))
          (repo (get-text-property 0 :repo file))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (user-error "The current user, %s, %s to edit this file" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (ref (or (get-text-property 0 :ref file)
                   (get-text-property 0 :ref (consult-gh--read-branch repo nil nil t nil))
                   (or consult-gh-default-branch-to-load "HEAD")))
          (ref (cond
                ((or (equal ref "HEAD") (equal ref ""))
                 (consult-gh--repo-get-default-branch repo))
                ((and (stringp ref)
                      (not (string-empty-p ref))
                      (substring-no-properties ref)))))
          (_ (when (stringp ref)
               (cond
                ((equal (get-text-property 0 :type ref) "sha")
                 (user-error "Cannot rename files in a commit ref"))
                ((equal (get-text-property 0 :type ref) "tag")
                 (user-error "Cannot rename files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'")))))
          (size (get-text-property 0 :size file))
          (mode (get-text-property 0 :mode file))
          (old-path (and (stringp file)
                         (not (string-empty-p file))
                         (get-text-property 0 :path file)))
          (old-path (and (stringp old-path)
                         (not (string-empty-p old-path))
                         old-path))
          (old-file-name (and (stringp old-path)
                              (file-name-nondirectory old-path)))
          (parent (cond
                   ((equal mode "directory")
                    (and (stringp old-path)
                         (file-name-directory old-path)))
                   (t
                    (and (stringp old-path)
                         (file-name-directory old-path)))))
          (new-file (consult-gh--files-read-file repo ref (if (stringp parent)
                                                              parent
                                                            nil)
                                                 old-file-name "Enter New File Path: " nil nil nil nil))
          (new-path (and (stringp new-file)
                         (not (string-empty-p new-file))
                         (get-text-property 0 :path new-file)))
          (new-path (and (stringp new-path)
                         (not (string-empty-p new-path))
                         new-path))
          (view-buffer (if (or (equal mode "file")
                               (equal mode "symlink"))
                           (consult-gh--files-view repo old-path nil t nil nil ref)))
          (content (and (bufferp view-buffer)
                        (buffer-live-p view-buffer)
                        (with-current-buffer view-buffer
                          (widen)
                          (buffer-substring-no-properties (point-min) (point-max))))))

     (when (stringp new-path)
       (add-text-properties 0 1 (list :repo repo
                                      :old-path (substring-no-properties old-path)
                                      :new-path (substring-no-properties new-path)
                                      :mode mode
                                      :size size
                                      :ref ref
                                      :class "file"
                                      :type "file"
                                      :object-type "blob"
                                      :view-buffer view-buffer
                                      :content content
                                      :new t)
                            new-path))
     (and repo old-path new-path
          (if (not (equal new-path old-path))
              (consult-gh--files-rename repo (list new-path) ref)
            (message "%s" (propertize "The new file path is the same as old one. Nothing to change!" 'face 'warning)))))))

;;;###autoload
(defun consult-gh-save-file (&optional file targetdir)
  "Interactively  save FILE in TARGETDIR.

Description of Arguments:
  FILE       a string with properties; the file to save
             for example see the local variable `consult-gh--topic'
             in  a buffer created by `consult-gh-find-file'.
  TARGETDIR  a string; the path of the folder where the files should
             be saved in."
  (interactive)
  (let* ((repo (or (and file (get-text-property 0 :repo file))
                   (and consult-gh--topic (get-text-property 0 :repo consult-gh--topic))
                   (get-text-property 0 :repo (consult-gh-search-repos nil t))))
         (file (or file
                   (and consult-gh--topic
                        (equal (get-text-property 0 :type consult-gh--topic) "file")
                        consult-gh--topic)
                   (consult-gh--files-read-file repo nil nil nil nil t t nil 'branch)))
         (ref (and (stringp file)
                   (get-text-property 0 :ref file)))
         (mode (get-text-property 0 :mode file))
         (path (get-text-property 0 :path file))
         (path (and (stringp path)
                    (not (string-empty-p path))
                    path))
         (targetdir (or targetdir consult-gh-default-save-directory default-directory)))
    (when (and repo path)
      (pcase mode
        ("directory"
         (let* ((dir-path (and (stringp path)
                               (file-name-as-directory path)))
                (files-list (consult-gh--files-nodirectory-items repo dir-path ref nil "30s"))
                (targetdir (if consult-gh-ask-for-path-before-save
                             (consult-gh--read-local-file nil "Save in Directory: " (and (stringp targetdir) (file-name-as-directory targetdir)) nil nil)
                             (and (stringp targetdir)
                                  (file-name-as-directory targetdir)))))
           (when (listp files-list)
             (mapc (lambda (f)
                     (when (and (consp f)
                                (plistp (cdr f)))
                       (let* ((info (cdr f))
                              (file-path (plist-get info :path))
                              (save-path (expand-file-name file-path targetdir)))
                         (when (and file-path save-path)
                          (consult-gh--files-save-file repo file-path save-path ref)))))
                   files-list))))
        ("file"
         (let* ((filename (and (stringp path) (file-name-nondirectory path)))
                (save-path (if consult-gh-ask-for-path-before-save
                               (file-truename (consult-gh--read-local-file nil "Enter File Save Path: " (expand-file-name filename (and (stringp targetdir) (file-name-as-directory targetdir))) nil nil))
                             (expand-file-name filename consult-gh-default-save-directory)))
                (save-path (if (and (stringp save-path)
                                    (file-directory-p save-path))
                               (expand-file-name filename save-path)
                             save-path)))
           (consult-gh--files-save-file repo path save-path ref)))))))

(defun consult-gh--notifications-items ()
  "Find all the user's notifications."
  (let* ((notifications (string-split (apply #'consult-gh--command-to-string (funcall consult-gh-notifications-args-func)) "      " t)))
    (cl-delete-duplicates (delq nil (mapcar (lambda (string) (consult-gh--notifications-format string))
                                            notifications)))))

(defun consult-gh--notifications (prompt &optional initial)
  "Search GitHub issues asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-search-issues'.

This runs the command line from `consult-gh--search-issues-builder' in
an async process and returns the results \(list of search results
for the input\) as a completion table in minibuffer.  The completion table
gets dynamically updated as the user types in the minibuffer.
Each candidate is formatted by `consult-gh--search-issues-transform'
to add annotation and other info to the candidate.

Description of Arguments:

  PROMPT  the prompt in the minibuffer
          \(passed as PROMPT to `consult--red'\)
  BUILDER an async builder function passed to `consult--process-collection'.
  INITIAL an optional arg for the initial input in the minibuffer.
          \(passed as INITITAL to `consult--read'\)"
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if-let* ((candidates (consult-gh--notifications-items)))
       (consult--read
        candidates
        :prompt prompt
        :lookup #'consult--lookup-member
        :state (funcall #'consult-gh--notifications-state)
        :initial initial
        :group #'consult-gh--notifications-group
        :require-match t
        :history 'consult-gh--notifications-history
        :category 'consult-gh-notifications
        :preview-key consult-gh-preview-key
        :sort nil)
     (progn (message "No new notifications!") nil))))

;;;###autoload
(defun consult-gh-notifications (&optional initial noaction prompt)
  "Search GitHub for User's work on GitHub.

This is an interactive wrapper function around
`consult-gh--dashboard'.

Upon selection of a candidate either
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to `consult-gh-issue-action'

INITIAL is an optional arg for the initial input in the minibuffer.

If PROMPT is non-nil, use it as the query prompt."
  (interactive)
  (let* ((prompt (or prompt "Select Notification:  "))
         (sel (consult-gh--notifications prompt initial)))
    (when (bound-and-true-p consult-gh-embark-mode)
        (setq consult-gh--last-command #'consult-gh-notifications))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (and (stringp sel) (funcall consult-gh-notifications-action sel)
           (consult-gh--notifications-mark-as-read sel)))))

(defun consult-gh--search-dashboard-transform (cands)
  "Add annotation to dashboard candidates in `consult-gh-dashboard'.

Format each candidate in CANDS with `consult-gh--dashboard-format'."
    (cl-loop for cand in cands
             collect (if (and (stringp cand) (not (string-empty-p cand)))
             (consult-gh--dashboard-format cand))))

(defun consult-gh--dashboard-issues-assigned-builder (input)
  "Find all the Issues/PRs assigned to `consult-gh--get-current-username'.

INPUT is passed as extra arguments to “gh search issues”."
  (pcase-let* ((cmd
                (append consult-gh-args (list "search" "issues" "--sort" "updated" "--assignee" "@me" "--json" "isPullRequest,repository,title,number,labels,updatedAt,state,url,commentsCount" "--template" (concat "{{range .}}" "{{.isPullRequest}}" "      " "{{.repository.nameWithOwner}}" "      " "{{.title}}" "      " "{{.number}}" "      " "{{.state}}" "      " "{{.updatedAt}}" "      " "{{.labels}}" "      " "{{.url}}" "      " "{{.commentsCount}}" "      "
 (format "Assigned to %s" (or (consult-gh--get-current-username) "me")) "\n" "{{end}}"))))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-s" flags) (member "--state" flags))
      (when (member consult-gh-dashboard-state-to-show '("open" "closed"))
      (setq opts (append opts (list "--state" (format "%s" consult-gh-dashboard-state-to-show))))))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-dashboard-maxnum)))))
    (cons (append cmd opts (remove nil (list arg))) nil)))

(defvar consult-gh--dashboard-assigned-to-user
  (list :name "Assigned to me"
        :narrow ?a
        :async (consult--process-collection #'consult-gh--dashboard-issues-assigned-builder
                 :transform (consult--async-transform #'consult-gh--search-dashboard-transform)
                 :min-input 0)
        :async-wrap (lambda (sink) (lambda (action) (if (stringp action) (funcall sink (propertize action 'consult--force t))
                                                      (funcall sink action))))
        :group #'consult-gh--dashboard-group
        :state #'consult-gh--dashboard-state
        :require-match t
        :category 'consult-gh-issues
        :preview-key consult-gh-preview-key
        :sort t)
"Source for dashboard items assigned to user.")

(defun consult-gh--dashboard-issues-authored-builder (input)
  "Find all the Issues/PRs authored by `consult-gh--get-current-username'.

INPUT is passed as extra arguments to “gh search issues”."
  (pcase-let* ((cmd
                (append consult-gh-args (list "search" "issues" "--sort" "updated" "--include-prs" "--author" "@me" "--json" "isPullRequest,repository,title,number,labels,updatedAt,state,url,commentsCount" "--template" (concat "{{range .}}" "{{.isPullRequest}}" "      " "{{.repository.nameWithOwner}}" "      " "{{.title}}" "      " "{{.number}}" "      " "{{.state}}" "      " "{{.updatedAt}}" "      " "{{.labels}}" "      " "{{.url}}" "      " "{{.commentsCount}}" "      " (format "Authored by %s" (or (consult-gh--get-current-username) "me")) "\n" "{{end}}"))))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-s" flags) (member "--state" flags))
      (when (member consult-gh-dashboard-state-to-show '("open" "closed"))
      (setq opts (append opts (list "--state" (format "%s" consult-gh-dashboard-state-to-show))))))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-dashboard-maxnum)))))
    (cons (append cmd opts (remove nil (list arg))) nil)))

(defvar consult-gh--dashboard-authored-by-user
  (list :name "Authored by me"
        :narrow ?w
        :async (consult--process-collection #'consult-gh--dashboard-issues-authored-builder
                 :transform (consult--async-transform #'consult-gh--search-dashboard-transform)
                 :min-input 0)
        :async-wrap (lambda (sink) (lambda (action) (if (stringp action) (funcall sink (propertize action 'consult--force t))
                                                      (funcall sink action))))
        :group #'consult-gh--dashboard-group
        :state #'consult-gh--dashboard-state
        :require-match t
        :category 'consult-gh-issues
        :preview-key consult-gh-preview-key
        :sort t)
"Source for dashboard items authored by user.")

(defun consult-gh--dashboard-issues-mentions-builder (input)
  "Find all the Issues/PRs that mentions `consult-gh--get-current-username'.

INPUT is passed as extra arguments to “gh search issues”."
  (pcase-let* ((cmd
                (append consult-gh-args (list "search" "issues" "--sort" "updated" "--include-prs" "--mentions" "@me" "--json" "isPullRequest,repository,title,number,labels,updatedAt,state,url,commentsCount" "--template" (concat "{{range .}}" "{{.isPullRequest}}" "      " "{{.repository.nameWithOwner}}" "      " "{{.title}}" "      " "{{.number}}" "      " "{{.state}}" "      " "{{.updatedAt}}" "      " "{{.labels}}" "      " "{{.url}}" "      " "{{.commentsCount}}" "      " (format "Mentions %s" (or (consult-gh--get-current-username) "me")) "\n" "{{end}}"))))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-s" flags) (member "--state" flags))
      (when (member consult-gh-dashboard-state-to-show '("open" "closed"))
      (setq opts (append opts (list "--state" (format "%s" consult-gh-dashboard-state-to-show))))))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-dashboard-maxnum)))))
    (cons (append cmd opts (remove nil (list arg))) nil)))

(defvar consult-gh--dashboard-mentions-user
  (list :name "Mentions me"
        :narrow ?m
        :async (consult--process-collection #'consult-gh--dashboard-issues-mentions-builder
                 :transform (consult--async-transform #'consult-gh--search-dashboard-transform)
                 :min-input 0)
        :async-wrap (lambda (sink) (lambda (action) (if (stringp action) (funcall sink (propertize action 'consult--force t))
                                                      (funcall sink action))))
        :group #'consult-gh--dashboard-group
        :state #'consult-gh--dashboard-state
        :require-match t
        :category 'consult-gh-issues
        :preview-key consult-gh-preview-key
        :sort t)
"Source for dashboard items that mentions user.")

(defun consult-gh--dashboard-issues-involves-builder (input)
  "Find all the Issues/PRs that involves `consult-gh--get-current-username'.

INPUT is passed as extra arguments to “gh search issues”."
  (pcase-let* ((user (consult-gh--get-current-username))
               (cmd
                (append consult-gh-args (list "search" "issues" "--sort" "updated" "--include-prs" "--involves" "@me" "--json" "isPullRequest,repository,title,number,labels,updatedAt,state,url,commentsCount" "--template" (concat "{{range .}}" "{{.isPullRequest}}" "      " "{{.repository.nameWithOwner}}" "      " "{{.title}}" "      " "{{.number}}" "      " "{{.state}}" "      " "{{.updatedAt}}" "      " "{{.labels}}" "      " "{{.url}}" "      " "{{.commentsCount}}" "      " (format "Involves %s" (or user "me")) "\n" "{{end}}") "--" (concat "-author:" (or user "@me")))))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-s" flags) (member "--state" flags))
      (when (member consult-gh-dashboard-state-to-show '("open" "closed"))
      (setq opts (append opts (list "--state" (format "%s" consult-gh-dashboard-state-to-show))))))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-dashboard-maxnum)))))
    (cons (append cmd opts (remove nil (list arg))) nil)))

(defvar consult-gh--dashboard-involves-user
  (list :name "Involves me"
        :narrow ?i
        :async (consult--process-collection #'consult-gh--dashboard-issues-involves-builder
                 :transform (consult--async-transform #'consult-gh--search-dashboard-transform)
                 :min-input 0)
        :async-wrap (lambda (sink) (lambda (action) (if (stringp action) (funcall sink (propertize action 'consult--force t))
                                                      (funcall sink action))))
        :group #'consult-gh--dashboard-group
        :state #'consult-gh--dashboard-state
        :require-match t
        :category 'consult-gh-issues
        :preview-key consult-gh-preview-key
        :sort t)
"Source for dashboard items that involves user.")

(defun consult-gh--dashboard (prompt &optional initial)
  "Search current user's work on GitHub.

This is a non-interactive internal function.
For the interactive version see `consult-gh-dashboard'.

This searches relevant (e.g. mentioned, authored, review-requested, etc.)
issues and pull-requests for the `consult-gh--get-current-username'.

Description of Arguments:

  PROMPT  the prompt in the minibuffer
          \(passed as PROMPT to `consult--red'\)
  INITIAL an optional arg for the initial input in the minibuffer.
          \(passed as INITITAL to `consult--read'\)"
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (consult--multi consult-gh-dashboard-items-sources
                   :prompt prompt
                   :initial initial
                   :group #'consult-gh--dashboard-group
                   :history '(:input consult-gh--dashboard-history)
                   :sort nil)))

;;;###autoload
(defun consult-gh-dashboard (&optional initial noaction prompt)
  "Search GitHub for current user's work.

This is an interactive wrapper function around `consult-gh--dashboard'.

Upon selection of a candidate either
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to
                           `consult-gh-dashboard-action'

INITIAL is an optional arg for the initial input in the minibuffer.

If PROMPT is non-nil, use it as the query prompt.

This command runs `consult--multi' with sources listed in
`consult-gh-dashboard-items-sources' to find all the relevant \(e.g.
assigned, authored, mentions, involves, ...\) issues and
pull requests to the current user \(i.e.
`consult-gh--get-current-username'\).  `consult-gh-dashboard-items-sources'
can be customized to add more sources \(i.e. define other searches as
relevant\).

Furthermore, additional commandline arguments can be passed in the
minibuffer input by typing “--” followed by command line arguments.
For example the user can enter the following in the minibuffer:

consult-gh -- -L 5

and the async process for each source in
`consult-gh-dashboard-items-sources' will take the extra argument \(e.g.
will run “gh search issues --author @me consult-gh -L 5”\), which searches
the issues for the term “consult-gh” and sets the limit for the maximum
number of results to 5.

For more details on consult--async functionalities, see `consult-grep' and
the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  (let* ((prompt (or prompt "Search Dashboard:  "))
         (sel (consult-gh--dashboard prompt initial)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0  :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (and (stringp (car sel)) (funcall consult-gh-dashboard-action (car sel))))))

(defun consult-gh--release-list-transform (input)
"Add annotation to release candidates in `consult-gh-release-list'.

Format each candidates with `consult-gh--release-list-format' and
INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--release-format cand input nil))))

(defun consult-gh--release-list-builder (input)
  "Build gh command line for listing releases of the INPUT repository.

INPUT must be the full name of a GitHub repository as a string
e.g. “armindarvish/consult-gh”."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-release-list-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-release-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-release-list (prompt builder &optional initial min-input)
  "List releases of GitHub repos asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-release-list'.

This runs the command line from `consult-gh--release-list-builder'
in an async process and returns the results \(list of releases
for a repository\) as a completion table in minibuffer.  The completion
table gets dynamically updated as the user types in the minibuffer to
change the entry.
Each candidate in the minibuffer is formatted by
`consult-gh--release-list-transform' to add annotation and other info
to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--read'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (let* ((initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't)
                          (consult-gh--get-repo-from-directory)
                        nil))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--release-list-transform)
                             :min-input min-input)
                           :prompt prompt
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--release-state)
                           :initial initial
                           :group #'consult-gh--release-group
                           :require-match t
                           :category 'consult-gh-releases
                           :add-history  (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                                (localrepo (consult-gh--get-repo-from-directory)))
                                           (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                                                 (append (list (when topicrepo topicrepo)
                                                               (when localrepo localrepo)
                                                               (when localrepo (concat localrepo " -- --author @me"))
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                           :history '(:input consult-gh--repos-history)
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-release-list (&optional initial noaction prompt min-input)
  "Interactively list issues of a GitHub repository.

This is an interactive wrapper function around `consult-gh--release-issue-list'.
With prefix ARG, first search for a repo using `consult-gh-search-repos',
then list issues of that selected repo with `consult-gh--async-release-list'.

It queries the user to enter the full name of a GitHub repository in the
minibuffer \(expected format is “OWNER/REPO”\), then fetches the list of
releases of that repository and present them as a minibuffer completion
table for selection.  The list of candidates in the completion table are
dynamically updated as the user changes the minibuffer input.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned.
 - if NOACTION is nil     candidate is passed to
   `consult-gh-release-action'.

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
armindarvish/consult-gh -- -L 100
and the async process will run
“gh release list --repo armindarvish/consult-gh -L 100”, which sets the limit
for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-repos-list' for quick
access in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer.
\(passed as INITITAL to `consult-gh--async-release-list'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-release-list'

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'"
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq initial (or initial (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos initial t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-release-list))
  (let* ((prompt (or prompt "Enter Repo Name:  "))
         (sel (consult-gh--async-release-list prompt #'consult-gh--release-list-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-release-action sel))))

;;;###autoload
(defun consult-gh-release-create (&optional repo tagname title body target)
  "Create a new release with TAGNAME, TITLE, and BODY for REPO.

TARGET is the branch to target for release
This mimicks the same interactive issue creation from “gh release create” in
the terminal.
For more details refer to the manual with “gh release create --help”."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((topic (or repo "new release"))
          (type "release")
          (repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (canwrite (consult-gh--user-canwrite repo))
          (author (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account)))))

     (if (not canwrite)
         (message "The current user, %s, %s to create a release in that repo" (propertize author 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))
       (let* ((tags (consult-gh--repo-get-tags repo t))
              (tagname (or tagname (consult--read tags
                                          :prompt "Select/Create a tag: ")))
              (tagname (and (stringp tagname) (not (string-empty-p tagname)) tagname))
              (title (or title (and tagname (consult--read nil
                                                       :prompt (concat "Title (optional) " (if tagname (format "[%s]" tagname)) ": ")
                                                       :default tagname))))
              (title (and title (stringp title) (not (string-empty-p title)) (propertize title :consult-gh-draft-title t 'rear-nonsticky t)))
              (target (or target (consult-gh--read-branch repo nil "Select target branch: " nil t)))
              (target (and (stringp target) (not (string-empty-p target)) (get-text-property 0 :ref target)))
              (body (or body (consult--read (list (cons "Blank" "")
                                                  (cons "Use generated notes as template" :generate))
                                            :prompt "Release notes: "
                                            :lookup #'consult--lookup-cdr)))
              (body (cond
                     ((equal body :generate)
                      (consult-gh--release-generate-notes repo tagname nil target topic))
                     ((stringp body)
                      body)))
              (body (and body (not (string-empty-p body)) (propertize body :consult-gh-draft-body t 'rear-nonsticky t)))
              (body (consult-gh--format-text-for-mode body))
              (buffer (format "*consult-gh-release-create: %s" repo)))
         ;; add properties to consult-gh--topic
         (add-text-properties 0 1 (list :title title :tagname tagname :type type :target target :isComment nil :new t :repo (substring-no-properties repo) :author author :initial-body body) topic)


         (consult-gh--completion-set-all-fields repo topic canwrite)

         ;; insert buffer contents
         (consult-gh-topics--insert-buffer-contents buffer topic :title title :body body :target target :tagname tagname :canwrite canwrite)

         ;; switch to buffer
         (funcall consult-gh-pop-to-buffer-func buffer))))))

(defun consult-gh-release-create-fill-body ()
  "Fill body of release draft with auto generated notes."
  (interactive nil consult-gh-topics-edit-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
  (let* ((release consult-gh--topic)
         (type (get-text-property 0 :type release)))
    (if (equal type "release")
        (let* ((repo (get-text-property 0 :repo release))
               (meta (consult-gh-topics--release-get-metadata release))
               (tag (cdr (assoc "tag" meta)))
               (target (cdr (assoc "target" meta)))
               (notes (get-text-property 0 :release-notes release))
               (previous-tag (and (plistp notes) (plist-get notes :previous-tag)))
               (body (consult-gh--release-generate-notes repo tag previous-tag target release))
               (body (consult-gh--format-text-for-mode body))
               (body-region (consult-gh--get-region-with-prop :consult-gh-draft-body))
               (body-beg (and body-region (car-safe (car-safe body-region))))
               (header-region (consult-gh--get-region-with-overlay :consult-gh-header))
               (header-end (and header-region (cdr-safe (car-safe (last header-region))))))
          (goto-char (or header-end body-beg (point-max)))
          (delete-region (point) (point-max))
          (when (and body (stringp body) (not (string-empty-p body)))
            (insert (propertize body :consult-gh-draft-body t 'rear-nonsticky t))))))))

;;;###autoload
(defun consult-gh-release-delete (&optional repo release noconfirm)
  "Interactively delete RELEASE of REPO.

It runs the command “gh release delete ...” to delete a release
using the internal function `consult-gh--release-delete'.

When NOCONFIRM is non-nil, do not ask for confirmation"
  (interactive)
  (when-let* ((repo (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))))
             (release (or release (get-text-property 0 :tagname (consult-gh-release-list repo t)))))
    (consult-gh-with-host
     (consult-gh--auth-account-host)
     (consult-gh--release-delete repo release (or noconfirm (not consult-gh-confirm-before-delete-release))))))

;;;###autoload
(defun consult-gh-release-edit (&optional release)
  "Edit the RELEASE.

This mimicks the same function as running “gh release edit” in the terminal.
For more details refer to the manual with “gh release edit --help”."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if (not consult-gh-release-view-mode)
       (let* ((repo (or (and release (get-text-property 0 :repo release))
                        (and consult-gh--topic (get-text-property 0 :repo consult-gh--topic))
                            (get-text-property 0 :repo (consult-gh-search-repos nil t))))
              (canwrite (consult-gh--user-canwrite repo))
              (release (or release
                      (and consult-gh--topic
                           (equal (get-text-property 0 :type consult-gh--topic) "release")
                           consult-gh--topic)
                      (consult-gh-release-list (if canwrite repo) t)))
              (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account)))))
         (if (not canwrite)
             (message "The current user, %s, %s to edit this release" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))
           (funcall #'consult-gh--release-view-action release)
           (consult-gh-release-edit)))
     (let* ((release consult-gh--topic)
            (repo (get-text-property 0 :repo release))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (if (not canwrite)
                   (message "The current user, %s, %s to edit this release" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
            (tagname (get-text-property 0 :tagname release))
            (title (get-text-property 0 :title release))
            (target (get-text-property 0 :target release))
            (draft (get-text-property 0 :draft release))
            (prerelease (get-text-property 0 :prerelease release))
            (discussion (get-text-property 0 :discussion release))
            (body (get-text-property 0 :body release))
            (newtopic (format "%s/%s" repo tagname))
            (buffer (format "*consult-gh-release-edit: %s %s" repo tagname))
            (type "release"))

       (add-text-properties 0 1 (list :isComment nil :type type :new nil :original-title title :original-body body :original-tagname tagname :original-target target :original-draft draft :original-prerelease prerelease :original-discussion discussion) newtopic)


       (with-timeout (1 nil)
         (while (not (plist-member (text-properties-at 0 release) :valid-release-tags))
           (sit-for 0.01)))
       (add-text-properties 0 1 (text-properties-at 0 release) newtopic)


       (with-current-buffer (consult-gh-topics--get-buffer-create buffer "release" newtopic)
         (unless (not (= (buffer-size) 0))

           ;; insert buffer contents
         (consult-gh-topics--insert-buffer-contents (consult-gh-topics--get-buffer-create buffer "release" newtopic)
                                                    newtopic :title title :body body :target target :tagname tagname :draft draft :prerelease prerelease :canwrite canwrite)
       (funcall consult-gh-pop-to-buffer-func buffer)))))))

;;;###autoload
(defun consult-gh-release-mark-draft (&optional release)
  "Mark RELEASE as draft."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((release (or release consult-gh--topic (consult-gh-release-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (get-text-property 0 :repo release))
          (type (get-text-property 0 :type release))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (message "The current user, %s, %s to edit this release" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (tagname (get-text-property 0 :tagname release))
          (args (list "release" "edit" tagname "--repo" repo "--draft")))
     (when (and canwrite (equal type "release"))
       (consult-gh--make-process (format "consult-gh-release-draft-%s-%s" repo tagname)
              :when-done (lambda (_event _str)
                            (message "Release %s in %s was marked as %s"
                                     (propertize tagname 'face 'consult-gh-pr)
                                     (propertize repo 'face 'consult-gh-repo)
                                     (propertize "DRAFT" 'face 'consult-gh-success))
                            (add-text-properties 0 1 (list :draft t) release))
              :cmd-args args)
       release))))

;;;###autoload
(defun consult-gh-release-toggle-prerelease (&optional release)
  "(Un)mark RELEASE as pre-release."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((release (or release consult-gh--topic (consult-gh-release-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (get-text-property 0 :repo release))
          (type (get-text-property 0 :type release))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (user-error "The current user, %s, %s to edit this release" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (tagname (get-text-property 0 :tagname release))
          (prerelease (get-text-property 0 :prerelease release))
          (prerelease (if (or (equal prerelease :false)
                              (equal prerelease "false"))
                          nil
                        prerelease))
          (args (list "release" "edit" tagname "--repo" repo)))
     (when (and canwrite (equal type "release"))
        (if prerelease
            (consult-gh--make-process (format "consult-gh-release-prerelease-%s-%s" repo tagname)
              :when-done (lambda (_event _str)
                            (message "Release %s in %s was %s"
                                     (propertize tagname 'face 'consult-gh-pr)
                                     (propertize repo 'face 'consult-gh-repo)
                                     (propertize "unmarked as PRE-RELEASE" 'face 'consult-gh-warning))
                            (add-text-properties 0 1 (list :prerelease nil) release))
              :cmd-args (append args (list "--prerelease=false")))
          (consult-gh--make-process (format "consult-gh-release-prerelease-%s-%s" repo tagname)
              :when-done (lambda (_ _str)
                            (message "Release %s in %s was %s"
                                     (propertize tagname 'face 'consult-gh-pr)
                                     (propertize repo 'face 'consult-gh-repo)
                                     (propertize "unmarked as PRE-RELEASE" 'face 'consult-gh-success))
                            (add-text-properties 0 1 (list :prerelease t) release))
              :cmd-args (append args (list "--prerelease"))))
        release))))

;;;###autoload
(defun consult-gh-release-mark-latest (&optional release)
  "Mark RELEASE as latest."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((release (or release consult-gh--topic (consult-gh-release-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (get-text-property 0 :repo release))
          (type (get-text-property 0 :type release))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (message "The current user, %s, %s to edit this release" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (tagname (get-text-property 0 :tagname release))
          (draft (get-text-property 0 :draft release))
          (draft (if (or (equal draft :false)
                         (equal draft "false"))
                     nil
                   draft))
          (prerelease (get-text-property 0 :prerelease release))
          (prerelease (if (or (equal prerelease :false)
                              (equal prerelease "false"))
                          nil
                        prerelease))
          (args (list "release" "edit" tagname "--repo" repo "--latest")))
     (when (and canwrite (equal type "release"))
       (if (or draft prerelease)
               (message "Latest release cannot be %s." (propertize "draft or prerelease" 'face 'consult-gh-error))

         (consult-gh--make-process (format "consult-gh-release-latest-%s-%s" repo tagname)
                                   :when-done (lambda (_event _str)
                                                 (message "Release %s in %s was %s"
                                                          (propertize tagname 'face 'consult-gh-pr)
                                                          (propertize repo 'face 'consult-gh-repo)
                                                          (propertize "marked as LATEST" 'face 'consult-gh-success))
                                                 (add-text-properties 0 1 (list :latest t) release))
                                   :cmd-args args)
         release)))))

;;;###autoload
(defun consult-gh-release-publish (&optional release)
  "Publish RELEASE."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((release (or release consult-gh--topic (consult-gh-release-list (get-text-property 0 :repo (consult-gh-search-repos nil t)) t)))
          (repo (get-text-property 0 :repo release))
          (type (get-text-property 0 :type release))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (if (not canwrite)
                 (message "The current user, %s, %s to edit this release" (propertize user 'face 'consult-gh-error) (propertize "does not have permission" 'face 'consult-gh-error))))
          (tagname (get-text-property 0 :tagname release))
          (args (list "release" "edit" tagname "--repo" repo "--draft=false" "--latest=false")))
     (when (and canwrite (equal type "release"))
       (consult-gh--make-process (format "consult-gh-release-publish-%s-%s" repo tagname)
              :when-done (lambda (_event _str)
                            (message "Release %s in repo %s was %s"
                                     (propertize tagname 'face 'consult-gh-pr)
                                     (propertize repo 'face 'consult-gh-repo)
                                     (propertize "PUBLISHED" 'face 'consult-gh-success))
                            (add-text-properties 0 1 (list :draft nil) release))
              :cmd-args args)
       release))))

;;;###autoload
(defun consult-gh-release-download (&optional repo tagname)
  "Download the release with TAGNAME from REPO.

This mimicks the same function as running
“gh release download TGNAME --repo REPO” in the terminal.

For more details refer to the manual with “gh release download --help”."
  (interactive)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or repo
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (tagname (or tagname
                       (and (stringp consult-gh--topic)
                            (get-text-property 0 :tagname consult-gh--topic))
                       (get-text-property 0 :tagname (consult-gh-release-list repo t)))))
     (consult-gh--release-download repo tagname))))

(defun consult-gh--workflow-list-transform (input)
"Add annotation to workflow candidates in `consult-gh-workflow-list'.

Format each candidates with `consult-gh--workflow-list-format' and
INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--workflow-format cand input nil))))

(defun consult-gh--workflow-list-builder (input)
  "Build gh command line for listing workflow actions of the INPUT repository.

INPUT must be the full name of a GitHub repository as a string
e.g. “armindarvish/consult-gh”."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-workflow-list-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-workflow-maxnum)))))
    (if consult-gh-workflow-show-all
         (setq opts (append opts (list "--all"))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts
                      (list "--template" consult-gh--workflow-list-template))
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-workflow-list (prompt builder &optional initial min-input)
  "List workflow actions of GitHub repos asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-workflow-list'.

This runs the command line from `consult-gh--workflow-list-builder'
in an async process and returns the results \(list of workflows
for a repository\) as a completion table in minibuffer.  The completion
table gets dynamically updated as the user types in the minibuffer to
change the entry.
Each candidate in the minibuffer is formatted by
`consult-gh--workflow-list-transform' to add annotation and other info
to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--read'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (let* ((initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't)
                          (consult-gh--get-repo-from-directory)
                        nil))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--workflow-list-transform)
                             :min-input min-input)
                           :prompt prompt
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--workflow-state)
                           :initial initial
                           :group #'consult-gh--workflow-group
                           :require-match t
                           :category 'consult-gh-workflows
                           :add-history  (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                                (localrepo (consult-gh--get-repo-from-directory)))
                                           (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                                                 (append (list (when topicrepo topicrepo)
                                                               (when localrepo localrepo)
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                           :history '(:input consult-gh--repos-history)
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-workflow-list (&optional initial noaction prompt min-input)
  "Interactively list workflow actions of a GitHub repository.

This is an interactive wrapper function around
`consult-gh--async-workflow-list'.  With prefix ARG, first
search for a repo using `consult-gh-search-repos', then list workflows
of that selected repo with `consult-gh--async-workflow-list'.

It queries the user to enter the full name of a GitHub repository in the
minibuffer \(expected format is “OWNER/REPO”\), then fetches the list of
workflows of that repository and present them as a minibuffer completion
table for selection.  The list of candidates in the completion table are
dynamically updated as the user changes the minibuffer input.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned.
 - if NOACTION is nil     candidate is passed to
   `consult-gh-workflow-action'.

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
armindarvish/consult-gh -- -L 100
and the async process will run
“gh workflow list --repo armindarvish/consult-gh -L 100”, which sets the limit
for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-repos-list' for quick
access in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer.
\(passed as INITITAL to `consult-gh--async-workflow-list'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-workflow-list'

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'"
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq initial (or initial (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos initial t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-workflow-list))
  (let* ((prompt (or prompt "Enter Repo Name:  "))
        (sel (consult-gh--async-workflow-list prompt #'consult-gh--workflow-list-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-workflow-action sel))))

;;;###autoload
(defun consult-gh-workflow-change-yaml-ref ()
"Show the yaml file of workflow from a different ref.

can be used in `consult-gh-workflow-view-mode' to see the yaml file
of a GitHub action from a different branc or tagname."
(interactive nil consult-gh-workflow-view-mode)
(let* ((inhibit-read-only t)
       (workflow (and (stringp consult-gh--topic)
                      (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                      consult-gh--topic))
       (repo (and workflow (get-text-property 0 :repo workflow)))
       (workflow-id (and workflow (get-text-property 0 :id workflow)))
       (ref-history (consult-gh--workflow-get-runs-refs-history repo workflow-id))
       (ref (consult-gh--read-ref repo nil "Select Branch or tag name with the version of the workflow to run: " t nil (if ref-history 'ref-history t) nil nil))
       (yaml-text (consult-gh--workflow-format-yaml repo workflow-id workflow ref))
       (regions (consult-gh--get-region-with-prop :consult-gh-workflow-yaml))
       (region (when (listp regions) (cons (caar regions) (cdar (last regions))))))
  (when yaml-text
    (add-text-properties 0 1 (list :ref ref) workflow)
    (when region
      (goto-char (car region))
      (delete-region (car region) (cdr region))
      (delete-region (line-beginning-position) (line-end-position))
      (insert "\n"))
    (save-excursion
        (insert (with-temp-buffer
                  (insert yaml-text)
                  (consult-gh--format-view-buffer "workflow")
                  (buffer-string)))))))

;;;###autoload
(defun consult-gh-workflow-enable (&optional workflow)
  "Enable a WORKFLOW.

WORKFLOW must be a propertized text describing a workflow similar to one
returned by `consult-gh-workflow-list'."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
     (let* ((repo (or (and (stringp workflow) (get-text-property 0 :repo workflow))
                      (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                      (get-text-property 0 :repo (consult-gh-search-repos nil t))))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (unless canwrite
                   (user-error "The current user, %s, %s to enable a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
            (workflow (or workflow
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                          consult-gh--topic)
                     (consult-gh-workflow-list repo t)))
            (workflow-id (and (stringp workflow)
                              (get-text-property 0 :id workflow))))
 (consult-gh--workflow-enable repo workflow-id))))

;;;###autoload
(defun consult-gh-workflow-disable (&optional workflow)
  "Disable a WORKFLOW.

WORKFLOW must be a propertized text describing a workflow similar to one
returned by `consult-gh-workflow-list'."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
     (let* ((repo (or (and (stringp workflow) (get-text-property 0 :repo workflow))
                      (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                      (get-text-property 0 :repo (consult-gh-search-repos nil t))))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (unless canwrite
                   (user-error "The current user, %s, %s to disable a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
            (workflow (or workflow
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                          consult-gh--topic)
                     (consult-gh-workflow-list repo t)))
            (workflow-id (and (stringp workflow)
                              (get-text-property 0 :id workflow))))

       (consult-gh--workflow-disable repo workflow-id))))

;;;###autoload
(defun consult-gh-workflow-run (&optional workflow ref)
  "Run a WORKFLOW in REF.

WORKFLOW must be a propertized text describing a workflow similar to
one returned by `consult-gh-workflow-list'.  REF is the branch or
tagname that contains the version of WORKFLOW to run."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
     (let* ((repo (or (and (stringp workflow) (get-text-property 0 :repo workflow))
                      (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                      (get-text-property 0 :repo (consult-gh-search-repos nil t))))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (unless canwrite
                   (user-error "The current user, %s, %s to run a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
            (workflow (or workflow
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                          consult-gh--topic)
                     (consult-gh-workflow-list repo t)))
            (workflow-id (and (stringp workflow)
                              (get-text-property 0 :id workflow)))
            (ref-history (and (stringp workflow)
                              (get-text-property 0 :ref-history workflow))))
(consult-gh--workflow-run repo workflow-id ref ref-history))))

;;;###autoload
(defun consult-gh-workflow-edit-yaml (&optional workflow ref)
  "Edit the yaml file of a WORKFLOW in REF.

WORKFLOW must be a propertized text describing a workflow similar to
one returned by `consult-gh-workflow-list'.  REF is the branch,
tagname or commit sha that contains the version of WORKFLOW to edit."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
     (let* ((repo (or (and (stringp workflow) (get-text-property 0 :repo workflow))
                      (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                      (get-text-property 0 :repo (consult-gh-search-repos nil t))))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (unless canwrite
                   (user-error "The current user, %s, %s to edit a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
            (workflow (or workflow
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                          consult-gh--topic)
                     (consult-gh-workflow-list repo t)))
            (workflow-path (and (stringp workflow)
                              (get-text-property 0 :path workflow)))
            (local-info (and consult-gh-workflow-view-mode
                             (get-text-property (point) :consult-gh)))
            (ref (or ref
                     (and (listp local-info) (plist-get local-info :ref))
                     (and (stringp workflow)
                      (or (get-text-property 0 :ref workflow)
                          (get-text-property 0 :last-ref workflow)))
                     (consult-gh--repo-get-default-branch repo))))
(consult-gh--workflow-edit-yaml repo workflow-path ref))))

;;;###autoload
(defun consult-gh-workflow-create (&optional repo branch)
  "Create a new workflow in REPO and BRANCH."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((repo (or repo
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (canwrite (consult-gh--user-canwrite repo))
          (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
          (_ (unless canwrite
               (user-error "The current user, %s, %s to create a workflow in repo, %s"
                           (propertize user 'face 'consult-gh-error)
                           (propertize "does not have permission" 'face 'consult-gh-error)
                           (propertize repo 'face 'consult-gh-repo))))
          (workflow (consult-gh--workflow-get-templates repo))
          (workflow-path (and (listp workflow)
                              (plistp (car-safe workflow))
                              (plist-get (car-safe workflow) :path)))
          (workflow-repo  (and (listp workflow)
                               (plistp (car-safe workflow))
                               (plist-get (car-safe workflow) :repo)))
          (content (cond
                    ((equal workflow-path 'read) consult-gh-workflow-template)
                    ((stringp workflow-path) (consult-gh--files-get-content-by-path (plist-get (car-safe workflow) :repo) workflow-path))))
          (content  (if (and (stringp workflow-path)
                             (not (member workflow-repo (list "actions/starter-workflows" repo)))
                             (stringp content))
                        (concat (format "# Source: %s/%s\n" workflow-repo workflow-path) content)
                      content))
          (file (cond
                 ((equal workflow-path 'read)
                  (consult-gh--files-read-file repo branch ".github/workflows/" nil "YAML File Name: " nil nil ".yml" 'branch))
                 ((stringp workflow-path)
                  (consult-gh--files-read-file repo branch ".github/workflows/" (file-name-nondirectory workflow-path) "YAML File Name: " nil nil ".yml" 'branch)))))
     (if file
         (consult-gh-create-file file content)
       (user-error "Did not get a name for the file!")))))

;;;###autoload
(defun consult-gh-workflow-edit (&optional workflow)
  "Edit the WORKFLOW.

WORKFLOW must be a propertized text describing a workflow similar to
one returned by `consult-gh-workflow-list'.  REF is the branch,
tagname or commit sha that contains the version of WORKFLOW to edit."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
     (let* ((repo (or (and (stringp workflow) (get-text-property 0 :repo workflow))
                      (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                      (get-text-property 0 :repo (consult-gh-search-repos nil t))))
            (canwrite (consult-gh--user-canwrite repo))
            (user (or (car-safe consult-gh--auth-current-account) (car-safe (consult-gh--auth-current-active-account))))
            (_ (unless canwrite
                   (user-error "The current user, %s, %s to edit a workflow in repo, %s"
                          (propertize user 'face 'consult-gh-error)
                          (propertize "does not have permission" 'face 'consult-gh-error)
                          (propertize repo 'face 'consult-gh-repo))))
            (workflow (or workflow
                     (and consult-gh--topic
                          (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                          consult-gh--topic)
                     (consult-gh-workflow-list repo t))))
       (pcase (consult--read (list (cons "Enable Workflow" :enable)
                                   (cons "Disable Workflow" :disable)
                                   (cons "Edit YAML file" :edit))
                             :prompt "What do you want to do? "
                             :lookup #'consult--lookup-cdr
                             :sort nil)
         (':enable (consult-gh-workflow-enable workflow))
         (':disable (consult-gh-workflow-disable workflow))
         (':edit (consult-gh-workflow-edit-yaml workflow))))))

(defun consult-gh--run-list-transform (input)
"Add annotation to run candidates in `consult-gh-run-list'.

Format each candidates with `consult-gh--run-list-format' and
INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--run-format cand input nil))))

(defun consult-gh--run-list-builder (workflow input)
  "Build gh command line for listing workflow run in INPUT repository.

WORKFLOW is an optional workflow id to further filter runs by a given
workflow.

INPUT must be the full name of a GitHub repository as a string
e.g. “armindarvish/consult-gh”."
  (pcase-let* ((consult-gh-args (append consult-gh-args consult-gh-run-list-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-workflow-maxnum)))))
    (unless (or (member "-w" flags) (member "--workflow" flags))
      (if workflow
          (setq opts (append opts (list "--workflow" workflow)))))
    (if consult-gh-run-show-all
         (setq opts (append opts (list "--all"))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts
                      (list "--template" consult-gh--run-list-template))
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-run-list (prompt builder &optional initial min-input)
  "List action run instances of GitHub repos asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-workflow-list'.

This runs the command line from `consult-gh--run-list-builder'
in an async process and returns the results \(list of runs
for a repository\) as a completion table in minibuffer.  The completion
table gets dynamically updated as the user types in the minibuffer to
change the entry.
Each candidate in the minibuffer is formatted by
`consult-gh--run-list-transform' to add annotation and other info
to the candidate.

Description of Arguments:

  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--read'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (let* ((initial (or initial
                      (if (equal consult-gh-prioritize-local-folder 't)
                          (consult-gh--get-repo-from-directory)
                        nil))))
    (consult-gh-with-host (consult-gh--auth-account-host)
                          (consult--read (consult--process-collection builder
                             :transform (consult--async-transform-by-input #'consult-gh--run-list-transform)
                             :min-input min-input)
                           :prompt prompt
                           :lookup #'consult--lookup-member
                           :state (funcall #'consult-gh--run-state)
                           :initial initial
                           :group #'consult-gh--run-group
                           :require-match t
                           :category 'consult-gh-runs
                           :add-history  (let* ((topicrepo (consult-gh--get-repo-from-topic))
                                                (localrepo (consult-gh--get-repo-from-directory)))
                                           (mapcar (lambda (item) (when (stringp item) (concat (consult-gh--get-split-style-character) item)))
                                                 (append (list (when topicrepo topicrepo)
                                                               (when localrepo localrepo)
                                                               (thing-at-point 'symbol))
                                                         consult-gh--known-repos-list)))
                           :history '(:input consult-gh--repos-history)
                           :preview-key consult-gh-preview-key
                           :sort nil))))

;;;###autoload
(defun consult-gh-run-list (&optional initial noaction prompt min-input workflow)
  "Interactively list action run instances of a GitHub repository.

This is an interactive wrapper function around `consult-gh--async-run-list'.
With prefix ARG, first search for a repo using `consult-gh-search-repos',
then list runs of that selected repo with `consult-gh--async-run-list'.

It queries the user to enter the full name of a GitHub repository in the
minibuffer \(expected format is “OWNER/REPO”\), then fetches the list of
action runs of that repository and present them as a minibuffer completion
table for selection.  The list of candidates in the completion table are
dynamically updated as the user changes the minibuffer input.

Upon selection of a candidate either
 - if NOACTION is non-nil candidate is returned.
 - if NOACTION is nil     candidate is passed to
   `consult-gh-run-action'.

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
armindarvish/consult-gh -- -L 100
and the async process will run
“gh run list --repo armindarvish/consult-gh -L 100”, which sets the limit
for the maximum number of results to 100.

User selection is tracked in `consult-gh--known-repos-list' for quick
access in the future \(added to future history list\) in future calls.

INITIAL is an optional arg for the initial input in the minibuffer.
\(passed as INITITAL to `consult-gh--async-run-list'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-run-list'

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'"
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq initial (or initial (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos initial t))))))
  (let* ((prompt (or prompt "Enter Repo Name:  "))
        (sel (consult-gh--async-run-list prompt (apply-partially #'consult-gh--run-list-builder workflow) initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-run-action sel))))

;;;###autoload
(defun consult-gh-run-view (&optional repo run-id workflow)
  "View run with RUN-ID of WORKFLOW in REPO in an Emacs buffer."
  (interactive)
  (let* ((topic consult-gh--topic)
         (workflow (or workflow
                       (and (stringp topic)
                            (equal (get-text-property 0 :type topic) "workflow")
                            topic)
                       (consult-gh-workflow-list (or repo (get-text-property 0 :repo (consult-gh-search-repos nil t))) t)))
         (repo  (or repo (and (stringp workflow) (get-text-property 0 :repo workflow))))
         (workflow-id (and (stringp workflow)
                           (get-text-property 0 :id workflow)))
         (pl (get-text-property (point) :consult-gh))
         (run-id (or run-id
                     (and (plistp pl)
                          (plist-get pl :run-id))
                     (and workflow-id
                     (get-text-property 0 :id (consult-gh-run-list repo t nil nil workflow-id)))))
         (cand (when (and repo run-id)
                 (propertize (format "%s" run-id) :repo repo :id run-id))))
    (when cand
        (funcall consult-gh-run-action cand))))

;;;###autoload
(defun consult-gh-run-rerun (&optional run job-id)
  "Rerun RUN.

RUN is a string with properties that identifies a run.  For an
example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--run-view'.  RUN defaults to
`consult-gh--topic', and if that is nil or not from a run, then the
user is asked to chose a run interactively.

If JOB-ID is non-nil, rerun the specific job with JOB-ID."
  (interactive)
  (let* ((repo (or (and (stringp run)
                        (equal (get-text-property 0 :type run) "run")
                        (get-text-property 0 :repo run))
                    (and (stringp consult-gh--topic)
                         (get-text-property 0 :repo consult-gh--topic))
                   (get-text-property 0 :repo (consult-gh-search-repos nil t))))
         (pl (get-text-property (point) :consult-gh))
         (run (or run
                  (and (stringp consult-gh--topic)
                       (equal (get-text-property 0 :type consult-gh--topic) "run")
                       consult-gh--topic)
                  (and (stringp consult-gh--topic)
                       (equal (get-text-property 0 :type consult-gh--topic) "workflow")
                       "workflow")
                  (consult-gh-run-list repo t)))
         (run-id (or (and (stringp run)
                          (get-text-property 0 :id run))
                     (and (plistp pl)
                          (plist-get pl :run-id))))
         (job-id (or job-id
                     (and (plistp pl)
                      (plist-get pl :job-id))))
         (cand (when (and repo run-id)
                 (propertize (format "%s/%s" repo run-id) :repo repo :id run-id :job-id job-id))))
    (when cand
         (funcall #'consult-gh--run-rerun-action cand))))

;;;###autoload
(defun consult-gh-branch-create (&optional repo ref branch-name sync)
"Create a new branch in REPO.

REF is the reference for creating a new branch.
BRANCH-NAME is the name of the new branch.
If SYNC is non-nil, process is run synchronously.

This is wrapper around `consult-gh--branch-create'."

(interactive)
(let* ((repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t))))))
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (consult-gh--branch-create repo ref branch-name sync))))

;;;###autoload
(defun consult-gh-branch-delete (&optional repo branch sync)
"Delete a BRANCH in REPO.

BRANCH is the name of branch to delete.
If SYNC is non-nil, process is run synchronously.

This is wrapper around `consult-gh--branch-delete'."

(interactive)
(let* ((repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t))))))
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (consult-gh--branch-delete repo branch sync))))

;;;###autoload
(defun consult-gh-topics-comment-create (&optional topic)
  "Interactively create a new comment on TOPIC.

TOPIC is a string with properties that identify a topic to comment on.
For and example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--pr-view' or `consult-gh--issue-view'.
TOPIC defaults to `consult-gh--topic', and if that is also nil,
then the user is asked to chose the TOPIC interactively."
  (interactive "P")
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((topic (or topic consult-gh--topic))
          (newtopic (substring-no-properties topic))
          (repo (or (and (stringp topic) (get-text-property 0 :repo topic))
                    (get-text-property 0 :repo (consult-gh-search-repos nil t))))
          (type (or (and (stringp topic) (get-text-property 0 :type topic))
                    (consult--read  (list (cons "Issues" "issue") (cons "Pull Requests" "pr"))
                                    :prompt "What topic are you looking for? "
                                    :lookup #'consult--lookup-cdr
                                    :require-match t
                                    :sort nil)))
          (add-to-review (if (get-text-property 0 :review-buffer topic)
                             (consult--read '(("Add comments to the existing review draft" . t)
                                              ("Submit a separate single comment" . nil))
                                            :prompt "There is an existing review draft for this PR.  What do you want to do?"
                                            :lookup #'consult--lookup-cdr
                                            :require-match t
                                            :sort nil)))
          (number (and (stringp topic) (get-text-property 0 :number topic)))
          (options (append (and (not add-to-review) (list (cons "The Issue/Pull request itself." type)))
                           (and (plist-get (get-text-property (point) :consult-gh) :file)
                                (list (cons "The file at point." "file")))
                           (and (plist-get (get-text-property (point) :consult-gh) :code)
                                (list (cons "The line of code at pont." "code")))
                           (and (not add-to-review) (plist-get (get-text-property (point) :consult-gh) :comment-id)
                                (list (cons "Reply to the comment at point." "reply")))))

          (target (and number
                       (if (length> options 1)
                           (consult--read options
                                          :prompt "What do you want to comment on?"
                                          :lookup #'consult--lookup-cdr
                                          :require-match t
                                          :sort nil)
                         (cdar options))))
          (buffer (concat (format "*consult-gh-comment: %s - %s #%s %s" repo type number target)
                          (and (equal target "code") (number-to-string (point))))))


     (add-text-properties 0 1 (text-properties-at 0 topic) newtopic)
     (add-text-properties 0 1 (list :new t :target target :isComment t :type "comment") newtopic)

     (when topic
       (cond
        ((member target '("pr" "issue" "discussion" "topic")))
        ((equal target "code")
         (let* ((info (get-text-property (point) :consult-gh))
                (pos (consult-gh--get-line-and-side-inside-diff))
                (side (car pos))
                (line (cadr pos))
                (startside (caddr pos))
                (startline (cadddr pos))
                (snippet (if (region-active-p)
                             (buffer-substring-no-properties
                              (region-beginning) (region-end))
                           (buffer-substring-no-properties
                            (pos-bol) (pos-eol)))))
           (if (not line)
               (error "Cannot comment on that line")
             (progn
               (setq info (append info (list :line line :side side :startline startline :startside startside :snippet snippet :subject-type "line")))
               (if add-to-review (setq target "review"))
               (add-text-properties 0 1 (list :comment-info info :target target) newtopic)))))
        ((equal target "file")
         (let ((info (get-text-property (point) :consult-gh)))
           (when add-to-review
             (if (y-or-n-p "Currently, there is no support for comments on files inside a review.  But, you can submit a comment on this file as an independent review.  Do you want to proceed? ")
                 (progn (setq target "review-comment")
                        (add-text-properties 0 1 (list :comment-info (append info (list :subject-type "file")) :target target) newtopic))
               (error "Canceled!")))))
        ((equal target "reply")
         (if-let* ((info (get-text-property (point) :consult-gh)))
             (add-text-properties 0 1 (list :comment-info info) newtopic)))))
     (cond
      (topic
       (funcall consult-gh-pop-to-buffer-func (consult-gh-topics--get-buffer-create buffer "comment" newtopic)))
      (type
       (pcase type
         ("issue"
          (funcall #'consult-gh--issue-view-action (consult-gh-issue-list repo t)))
         ("pr"
          (funcall #'consult-gh--pr-view-action (consult-gh-pr-list repo t)))))
      (t (message "Did not get a topic to comment on!"))))))

;;;###autoload
(defun consult-gh-topics-cancel ()
  "Cancel the current topic or comment."
  (interactive nil consult-gh-topics-edit-mode)
  (funcall consult-gh-quit-window-func t nil)
  (message "Canceled!"))

;;;###autoload
(defun consult-gh-topics-submit (&optional topic)
  "Submit TOPIC.

TOPIC is a string with properties that identify a consult-gh topic such as
a comment, issue, pull request, pull request review, etc.
For an example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by commands such as `consult-gh-pr-create',
`consult-gh-issue-create', `consult-gh--pr-view', `consult-gh--issue-view',
`consult-gh-pr-review', or `consult-gh-topics-comment-create'.
TOPIC defaults to `consult-gh--topic', and if that is also nil,
then the user is asked to chose the TOPIC interactively."
  (interactive "P" consult-gh-topics-edit-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (if consult-gh-topics-edit-mode
       (let* ((topic (or topic consult-gh--topic))
              (repo (get-text-property 0 :repo topic))
              (type (get-text-property 0 :type topic))
              (isComment (get-text-property 0 :isComment topic))
              (target (get-text-property 0 :target topic))
              (number (get-text-property 0 :number topic))
              (new (get-text-property 0 :new topic)))
         (cond
          ((and isComment (member target '("issue" "pr" "discussion" "topic")))
           (let ((comment (consult-gh-topics--buffer-string)))
             (and (consult-gh-topics--topic-comment-submit comment repo target number)
                  (message "Comment Submitted!")
                  (funcall consult-gh-quit-window-func t))))
          ((and isComment (equal target "code"))
           (let* ((comment (consult-gh-topics--buffer-string))
                  (info (get-text-property 0 :comment-info topic))
                  (commit-id (plist-get info :commit-id))
                  (path (plist-get info :path))
                  (line (plist-get info :line))
                  (side (plist-get info :side))
                  (startline (plist-get info :startline))
                  (startside (plist-get info :startside)))
             (and (consult-gh-topics--code-comment-submit comment repo number commit-id path line side startline startside)
                  (message "Comment Submitted!")
                  (funcall consult-gh-quit-window-func t))))
          ((and isComment (equal target "file"))
           (let* ((comment (consult-gh-topics--buffer-string))
                  (info (get-text-property 0 :comment-info topic))
                  (commit-id (plist-get info :commit-id))
                  (path (plist-get info :path)))
             (and (consult-gh-topics--file-comment-submit comment repo number commit-id path)
                  (message "Comment Submitted!")
                  (funcall consult-gh-quit-window-func t))))
          ((and isComment (equal target "reply"))
           (let* ((comment (consult-gh-topics--buffer-string))
                  (info (get-text-property 0 :comment-info topic))
                  (comment-id (plist-get info :comment-id))
                  (reply-url (plist-get info :reply-url)))
             (and (consult-gh-topics--reply-comment-submit comment comment-id reply-url)
                  (message "Comment Submitted!")
                  (funcall consult-gh-quit-window-func t))))
          ((and isComment (equal target "review-comment"))
           (when (y-or-n-p "This will submit this one comment as a pull request review.  Do you want to proceed?")
           (consult-gh-topics--pr-review-comment-submit topic)))
          ((and isComment (equal target "review"))
           (consult-gh-topics--pr-review-append-comment topic))
          ((and new (equal target "review"))
           (consult-gh-topics--pr-review-presubmit topic))
          ((and (not new) (equal type "repo"))
           (consult-gh-topics--edit-repo-presubmit topic))
          ((and new (equal type "issue"))
           (consult-gh-topics--issue-create-presubmit topic))
          ((and (not new) (equal type "issue"))
           (consult-gh-topics--edit-issue-presubmit topic))
          ((and new (equal type "pr"))
           (consult-gh-topics--pr-create-presubmit topic))
          ((and (not new) (equal type "pr"))
           (consult-gh-topics--edit-pr-presubmit topic))
          ((and new (equal type "release"))
           (consult-gh-topics--release-create-presubmit topic))
          ((and (not new) (equal type "release"))
           (consult-gh-topics--edit-release-presubmit topic))
          ((and new (equal type "merge commit"))
           (consult-gh-topics--pr-merge-presubmit topic))
          ((and new (equal type "commit message"))
           (consult-gh-topics--commit-submit topic))))
     (message "Not in a consult-gh topic editing buffer!"))))

;;;###autoload
(defun consult-gh-topics-open-in-browser (&optional topic)
  "Open the TOPIC of the current buffer in the browser.

TOPIC is a text with properties that identify a consult-gh topic such as
a comment, issue, pull request, pull request review, etc.
For an example, see the buffer-local variable `consult-gh--topic' in the
buffer generated by commands such as `consult-gh-pr-create',
`consult-gh-issue-create' `consult-gh--pr-view', `consult-gh--issue-view',
`consult-gh-pr-review', or `consult-gh-topics-comment-create'.
TOPIC defaults to `consult-gh--topic'.

This function uses `consult-gh-browse-url-func' for opening a url in the
browser."
  (interactive "P" consult-gh-pr-view-mode consult-gh-issue-view-mode consult-gh-workflow-view-mode consult-gh-commit-view-mode consult-gh-misc-view-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((topic (or topic consult-gh--topic))
          (type (and (stringp topic) (get-text-property 0 :type topic)))
          (repo (and (stringp topic) (get-text-property 0 :repo topic)))
          (branch (and (stringp topic) (get-text-property 0 :ref topic)))
          (path (and (stringp topic) (get-text-property 0 :path topic)))
          (number (and (stringp topic) (get-text-property 0 :number topic)))
          (tagname (and (stringp topic) (get-text-property 0 :tagname topic)))
          (ref (and (stringp topic) (get-text-property 0 :ref topic)))
          (sha (and (stringp topic) (get-text-property 0 :sha topic)))
          (id (and (stringp topic) (get-text-property 0 :id topic)))
          (local-info (get-text-property (point) :consult-gh))
          (local-url (or (plist-get local-info :url)
                         (plist-get local-info :comment-url)
                         (plist-get local-info :commit-url)
                         (plist-get local-info :yaml-url)))
          (topic-url (get-text-property 0 :url topic))
          (url (or local-url
                   topic-url
                   (and (stringp type)
                        (pcase type
                          ("repo"
                           (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")))
                          ("file"
                           (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/blob/%s/%s" branch path)))
                          ("issue"
                           (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/issues/%s" number)))
                          ("pr"
                           (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/pull/%s" number)))
                          ("release"
                           (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/releases/%s" tagname)))
                          ("workflow"
                           (and (stringp path)
                                (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser"))
                                           (format "/actions/workflows/%s" (file-name-nondirectory path)))))
                          ("run"
                           (and id (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/actions/runs/%s" id))))
                          ("compare"
                           (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/compare/%s" ref)))
                          ("commit"
                           (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/commit/%s" sha))))))))
     (if (stringp url)
         (funcall (or consult-gh-browse-url-func #'browse-url) url)
       (message "No url to browse in this buffer!")))))

;;;###autoload
(defun consult-gh-commit-save-message (&optional commit-message)
  "Save COMMIT-MESSAGE to `log-edit-comment-ring'.

COMMIT-MESSAGE defaults to
`consult-gh--commit-get-buffer-message'.

Adapted from magit:
URL `https://github.com/magit/magit/blob/731642756f504c8a56d3775960b7af0a93c618bb/lisp/git-commit.el#L796'"
  (interactive nil consult-gh-commit-message-mode)
  (let ((message (or commit-message (consult-gh--commit-get-buffer-message))))
    (if message
        (progn
          (when-let* ((index (ring-member log-edit-comment-ring message)))
            (ring-remove log-edit-comment-ring index))
          (ring-insert log-edit-comment-ring message)
             (message "Message saved"))
           (message "Only whitespace and/or comments; message not saved"))))

;;;###autoload
(defun consult-gh-commit-prev-message (arg)
  "Cycle backward through message history, after saving current message.
With a numeric prefix ARG, go back ARG messages.

Adapted from magit:
URL `https://github.com/magit/magit/blob/731642756f504c8a56d3775960b7af0a93c618bb/lisp/git-commit.el#L740'"
  (interactive "*p" consult-gh-commit-message-mode)
  (let ((len (ring-length log-edit-comment-ring)))
    (if (<= len 0)
        (message "%s comment ring!" (propertize "Empty" 'face 'consult-gh-warning))
      (let* ((message (consult-gh--commit-get-buffer-message)))
        (when (and (stringp message)
                   (not (ring-member log-edit-comment-ring message)))
          (ring-insert log-edit-comment-ring message)
          (cl-incf arg)
          (setq len (ring-length log-edit-comment-ring))))
      ;; Delete the message but not the instructions at the end.
      (save-restriction
        (goto-char (point-min))
        (narrow-to-region
         (point)
         (if (re-search-forward (concat "^" comment-start) nil t)
             (max 1 (- (point) 2))
           (point-max)))
        (delete-region (point-min) (point)))
      (setq log-edit-comment-ring-index (log-edit-new-comment-index arg len))
      (message "Comment %d" (1+ log-edit-comment-ring-index))
      (insert (ring-ref log-edit-comment-ring log-edit-comment-ring-index)))))

;;;###autoload
(defun consult-gh-commit-next-message (arg)
  "Cycle forward through message history, after saving current message.
With a numeric prefix ARG, go forward ARG messages.

Adapted from magit:
URL `
https://github.com/magit/magit/blob/731642756f504c8a56d3775960b7af0a93c618bb/lisp/git-commit.el#L768'"
  (interactive "*p" consult-gh-commit-message-mode)
  (consult-gh-commit-prev-message (- arg)))

;;;###autoload
(defun consult-gh-commit-view-diff (&optional commit noselect)
  "View diff for COMMIT.

When NOSELECT is non-nil, do not switch to buffer
COMMIT is a string with properties that identify a commit.
For an example see the buffer-local variable `consult-gh--topic' in the
buffer generated by `consult-gh--commit-create'."
  (interactive nil consult-gh-commit-message-mode)
  (if consult-gh-commit-message-mode
      (let* ((commit (or commit consult-gh--topic))
             (buffer (consult-gh--commit-get-diff-buffer commit)))
        (unless noselect
          (funcall consult-gh-pop-to-buffer-func buffer))
        buffer)
    (progn (message "Not in a consult-gh commit message buffer!")
           nil)))

(defun consult-gh--commit-list (repo ref &optional link initial prompt)
  "Get list of commits for REPO and REF.

This fetches a list of commtis for REPO and REF from GitHub API.

REF is name of a branch or tag in REPO.

If optional argument LINK in non-nil, use it as API
link to fetch commits.

INITIAL is passed to `consult--read'.
PROMPT is passed to `consult--read'.

This is a non-interactive internal function.
For the interactive version see `consult-gh-commit-list'."
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((commits-and-links (consult-gh--commit-list-commits-and-links repo ref link))
          (commits-list (consult-gh--commit-parse-commits-and-links commits-and-links))
          (commits-list (and (listp commits-list)
                        (cl-loop for commit in commits-list
                                 collect
                                 (consult-gh--commit-list-format commit repo ref))))

           (prompt (or prompt (format "Select A Commit in %s [%s]:  " repo ref)))
           (sel
            (consult--read commits-list
                    :prompt prompt
                    :lookup #'consult--lookup-member
                    :state (consult-gh--commit-state)
                    :initial initial
                    :group #'consult-gh--commit-group
                    :require-match t
                    :history 'consult-gh--commit-list-history
                    :category 'consult-gh-commits
                    :preview-key consult-gh-preview-key
                    :sort nil)))
(cond
 ((and (stringp sel)
      (not (string-empty-p sel))
      (get-text-property 0 :title sel)
      (get-text-property 0 :link sel))
  (consult-gh--commit-list repo ref (get-text-property 0 :link sel) initial prompt))
 (t
  sel)))))

;;;###autoload
(defun consult-gh-commit-list (&optional repo ref initial noaction prompt)
  "Interactively list GitHub commits of REPO and REF.

REF is name of a branch or tag in REPO.
This is an interactive wrapper function around
`consult-gh--commit-list'.

It queries the user to select a commit from list of commits for
REPO and REF.

Upon selection of a candidate either
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to `consult-gh-commit-action'

INITIAL is an optional arg for the initial input in the minibuffer
\(passed as INITITAL to `consult-gh---commit-list'\).

If PROMPT is non-nil, use it as the query prompt."
  (interactive)
  (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
      (setq repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-commit-list))
  (let* ((repo (or repo
                   (and (stringp consult-gh--topic)
                        (get-text-property 0 :repo consult-gh--topic))
                   (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t)))))
         (ref (or ref (consult-gh--read-ref repo nil nil t)))
         (prompt (or prompt (format "Select a commit [%s]:  " (propertize ref 'face 'consult-gh-branch))))
         (sel (consult-gh--commit-list repo ref nil initial prompt)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-commit-action sel))))

(defun consult-gh--search-commits-transform (input)
  "Add annotation to commit candidates in `consult-gh-search-commits'.

Format each candidates with `consult-gh--search-commits-format' and INPUT."
  (lambda (cands)
    (cl-loop for cand in cands
             collect
             (consult-gh--search-commits-format cand input nil))))

(defun consult-gh--search-commits-builder (repo input)
  "Build gh command line for searching commits of REPO with INPUT."
  (pcase-let* ((consult-gh-args (append consult-gh-args (list "--repo" (format "%s" repo)) consult-gh-search-commits-args))
               (cmd (consult--build-args consult-gh-args))
               (`(,arg . ,opts) (consult-gh--split-command input))
               (flags (append cmd opts)))
    (unless (or (member "-L" flags) (member "--limit" flags))
      (setq opts (append opts (list "--limit" (format "%s" consult-gh-commit-maxnum)))))
    (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
      (if re
        (cons (append cmd
                      (list (string-join re " "))
                      opts)
              hl)
        (cons (append cmd opts) nil)))))

(defun consult-gh--async-search-commits (repo prompt builder &optional initial min-input)
  "Search GitHub commits in REPO asynchronously.

This is a non-interactive internal function.
For the interactive version see `consult-gh-search-commits'.

This runs the command line from `consult-gh--search-commits-builder' in
an async process and returns the results \(list of search results
for the input\) as a completion table in minibuffer.  The completion table
gets dynamically updated as the user types in the minibuffer.
Each candidate is formatted by `consult-gh--search-commits-transform'
to add annotation and other info to the candidate.

Description of Arguments:
  REPO      full name of respository
            \(e.g., armindarvish/consult-gh\)
  PROMPT    the prompt in the minibuffer
            \(passed as PROMPT to `consult--red'\)
  BUILDER   an async builder function passed to
            `consult--process-collection'.
  INITIAL   an optional arg for the initial input in the minibuffer.
            \(passed as INITITAL to `consult--read'\)
  MIN-INPUT is the minimum input length and defaults to
            `consult-async-min-input'"
  (consult-gh-with-host
   (consult-gh--auth-account-host)
      (consult--read
       (consult--process-collection (apply-partially builder repo)
         :transform (consult--async-transform-by-input #'consult-gh--search-commits-transform)
         :min-input min-input)
       :prompt prompt
       :lookup #'consult--lookup-member
       :state (consult-gh--commit-state)
       :initial initial
       :group #'consult-gh--commit-group
       :require-match t
       :history '(:input consult-gh--search-commits-history)
       :category 'consult-gh-commits
       :preview-key consult-gh-preview-key
       :sort nil)))

;;;###autoload
(defun consult-gh-search-commits (&optional repo initial noaction prompt min-input)
  "Interactively search GitHub commits of REPO.

This is an interactive wrapper function around
`consult-gh--async-search-commits'.

It queries the user for a search term in the minibuffer, then fetches the
list of possible GitHub commits for the entered query and presents them as a
minibuffer completion table for selection.  The list of candidates in the
completion table are dynamically updated as the user changes the entry.

Upon selection of a candidate either
 - if NOACTION is non-nil  candidate is returned
 - if NOACTION is nil      candidate is passed to `consult-gh-commit-action'

Additional command line arguments can be passed in the minibuffer input
by typing `--` followed by command line arguments.
For example the user can enter the following in the minibuffer:
consult-gh -- -L 100
and the async process will run “gh search commits bug fix --repo REPO -L 100”,
which sets the limit for the maximum number of results to 100.

INITIAL is an optional arg for the initial input in the minibuffer
\(passed as INITITAL to `consult-gh--async-search-commits'\).

If PROMPT is non-nil, use it as the query prompt.

MIN-INPUT is passed to `consult-gh--async-search-commits'.

For more details on consult--async functionalities, see `consult-grep'
and the official manual of consult, here:
URL `https://github.com/minad/consult'."
  (interactive)
  ;; (if (xor current-prefix-arg consult-gh-use-search-to-find-name)
  ;;     (setq repo (or repo (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t))))))
  (when (bound-and-true-p consult-gh-embark-mode)
    (setq consult-gh--last-command #'consult-gh-search-commits))
  (let* ((repo (or repo
                   (and (stringp consult-gh--topic)
                        (get-text-property 0 :repo consult-gh--topic))
                   (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t)))))
         (prompt (or prompt "Search Commits:  "))
         (sel (consult-gh--async-search-commits repo prompt #'consult-gh--search-commits-builder initial min-input)))
    ;;add org and repo to known lists
    (when-let* ((reponame (and (stringp sel) (get-text-property 0 :repo sel))))
      (add-to-history 'consult-gh--known-repos-list reponame))
    (when-let* ((username (and (stringp sel) (get-text-property 0 :user sel))))
      (add-to-history 'consult-gh--known-orgs-list username))
    (if noaction
        sel
      (funcall consult-gh-commit-action sel))))

;;;###autoload
(defun consult-gh-commit-view-comments (&optional commit)
  "Insert comments on COMMIT in the current buffer."
  (interactive nil consult-gh-commit-view-mode)
  (when consult-gh-commit-view-mode
    (let* ((commit (or commit consult-gh--topic))
           (inhibit-read-only t)
           (repo (get-text-property 0 :repo commit))
           (sha (get-text-property 0 :sha commit))
           (comments (consult-gh--commit-get-comments repo sha))
           (url (concat (string-trim (consult-gh--command-to-string "browse" "--repo" (string-trim repo) "--no-browser")) (format "/commit/%s" sha)))
           (comments-text (when (and comments (listp comments))
                            (consult-gh--commit-format-comments comments url)))
           (comments-text (or comments-text ""))
           (regions (consult-gh--get-region-with-prop :consult-gh-commit-comments))
           (region (when (listp regions) (cons (caar regions) (cdar (last regions))))))
      (when comments-text
        (when region
          (goto-char (car region))
          (delete-region (car region) (cdr region))
          (delete-region (line-beginning-position) (line-end-position))
          (insert "\n"))
        (save-excursion
          (insert (with-temp-buffer
                    (insert comments-text)
                    (consult-gh--format-view-buffer "commit")
                    (buffer-string)))
          (outline-hide-sublevels 2))))))

;;;###autoload
(defun consult-gh-commit-browse-files (&optional repo sha path noaction initial)
  "Browse files in REPO at sommit SHA.

Description of Arguments:
  REPO          a string; repository's full name
                \(e.g., armindarvish/consult-gh\)
  SHA           a string; commit sha
  PATH          a string; when non-nil search is done relative to PATH
  INITIAL       a string; used as initial input in searching files
                \(gets passed to `consult--read'\).
  NOACTION      a boolean; if non-nil, only retrun the file object
                and do not call `consult-gh-file-action'."
  (interactive)
  (setq consult-gh--open-files-list nil
        consult-gh--current-tempdir (consult-gh--tempdir))
  (let* ((topic  (and (stringp consult-gh--topic)
                        (equal (get-text-property 0 :type consult-gh--topic) "commit")
                       consult-gh--topic))
         (repo (or repo
                   (and (stringp topic)
                        (get-text-property 0 :repo topic))
                   (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t)))))
         (sha (or sha
                   (and (stringp topic)
                        (get-text-property 0 :sha topic)))))
    (when (and sha
               (stringp sha))
      (add-text-properties 0 1 (list :type "sha") sha))
    (consult-gh-find-file repo sha path noaction initial)))

;;;###autoload
(defun consult-gh-ctrl-c-ctrl-c ()
  "Submit topic or invoke `org-ctrl-c-ctrl-c' in `org-mode'."
  (interactive)
  (cond
   ((and (derived-mode-p 'org-mode)
         (or consult-gh-topics-edit-mode
             consult-gh-repo-view-mode
             consult-gh-issue-view-mode)
         (org-in-src-block-p))
    (org-ctrl-c-ctrl-c))
   ((or consult-gh-pr-view-mode consult-gh-issue-view-mode)
    (consult-gh-topics-comment-create))
   (consult-gh-workflow-view-mode
    (let* ((in-block (equal (and (derived-mode-p 'org-mode)
                                 (org-in-src-block-p)
                                 (car (org-babel-get-src-block-info)))
                            "yaml"))
           (props (get-text-property (point) :consult-gh))
           (yaml-url (and (plistp props) (plist-get props :yaml-url)))
           (run-id (and (plistp props) (plist-get props :run-id))))
      (cond
       ((or in-block yaml-url)
        (consult-gh-workflow-run))
       (run-id
        (consult-gh-run-view))
       (t
        (pcase (consult--read (list (cons "Run Workflow" :run)
                                    (cons "View A Run Details" :view)
                                    (cons "Cacnel" :cancel))
                              :prompt "What would you like to do?"
                              :lookup #'consult--lookup-cdr
                              :sort nil)
          (':run (consult-gh-workflow-run))
          (':view (consult-gh-run-view)))))))
   (consult-gh-run-view-mode
    (consult-gh-run-rerun))
   (consult-gh-topics-edit-mode
    (consult-gh-topics-submit))))

;;;###autoload
(defun consult-gh-enable-default-keybindings ()
  "Enable default keybindings for all minor modes in consult-gh."
  (interactive)
  ;; consult-gh-repo-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-repo-view-mode-map  consult-gh--repo-view-mode-keybinding-alist)

  ;; consult-gh-issue-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-issue-view-mode-map  consult-gh--issue-view-mode-keybinding-alist)

  ;; consult-gh-pr-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-pr-view-mode-map  consult-gh--pr-view-mode-keybinding-alist)

  ;; consult-gh-release-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-release-view-mode-map  consult-gh--release-view-mode-keybinding-alist)

  ;; consult-gh-workflow-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-workflow-view-mode-map  consult-gh--workflow-view-mode-keybinding-alist)

  ;; consult-gh-run-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-run-view-mode-map  consult-gh--run-view-mode-keybinding-alist)

  ;; consult-gh-file-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-file-view-mode-map  consult-gh--file-view-mode-keybinding-alist)

  ;; consult-gh-misc-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-misc-view-mode-map  consult-gh--misc-view-mode-keybinding-alist)

  ;; consult-gh-topics-edit-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-topics-edit-mode-map consult-gh--topics-edit-mode-keybinding-alist)

  ;; consult-gh-commit-message-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-commit-message-mode-map consult-gh--commit-message-mode-keybinding-alist)

  ;; consult-gh-commit-view-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-commit-view-mode-map consult-gh--commit-view-mode-keybinding-alist)

  ;; consult-gh-upload-files-mode-map
  (consult-gh--enable-keybindings-alist consult-gh-upload-files-mode-map consult-gh--upload-files-mode-keybinding-alist))

;;;###autoload
(defun consult-gh-disable-default-keybindings ()
  "Disable default keybindings for minor modes in consult-gh."
  (interactive)
  ;; consult-gh-repo-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-repo-view-mode-map  consult-gh--repo-view-mode-keybinding-alist)

  ;; consult-gh-issue-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-issue-view-mode-map  consult-gh--issue-view-mode-keybinding-alist)

  ;; consult-gh-pr-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-pr-view-mode-map  consult-gh--pr-view-mode-keybinding-alist)

  ;; consult-gh-release-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-release-view-mode-map  consult-gh--release-view-mode-keybinding-alist)

   ;; consult-gh-workflow-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-workflow-view-mode-map  consult-gh--workflow-view-mode-keybinding-alist)

  ;; consult-gh-run-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-run-view-mode-map  consult-gh--run-view-mode-keybinding-alist)

  ;; consult-gh-file-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-file-view-mode-map  consult-gh--file-view-mode-keybinding-alist)

  ;; consult-gh-misc-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-misc-view-mode-map  consult-gh--misc-view-mode-keybinding-alist)

  ;; consult-gh-topics-edit-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-topics-edit-mode-map consult-gh--topics-edit-mode-keybinding-alist)

  ;; consult-gh-commit-message-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-commit-message-mode-map consult-gh--commit-message-mode-keybinding-alist)

  ;; consult-gh-commit-view-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-commit-view-mode-map consult-gh--commit-view-mode-keybinding-alist)

  ;; consult-gh-upload-files-mode-map
  (consult-gh--disable-keybindings-alist consult-gh-upload-files-mode-map consult-gh--upload-files-mode-keybinding-alist))

;;;###autoload
(defun consult-gh-kill-preview-buffers ()
  "Kill all open preview buffers stored in `consult-gh--preview-buffers-list'.

It asks for confirmation if the buffer is modified
and removes the buffers that are killed from the list."
  (interactive)
  (when consult-gh--preview-buffers-list
    (mapcar (lambda (buff) (if (buffer-live-p buff)
                               (kill-buffer buff))
              (unless (buffer-live-p buff)
                (setq consult-gh--preview-buffers-list
                      (delete buff consult-gh--preview-buffers-list))))
            consult-gh--preview-buffers-list)))

;;;###autoload
(defun consult-gh-kill-open-file-buffers ()
  "Kill all open file buffers stored in `consult-gh--open-files-buffers'.

It asks for confirmation if the buffer is modified
and removes the buffers that are killed from the list."
  (interactive)
  (when (and consult-gh--open-files-buffers
             (listp consult-gh--open-files-buffers))
    (mapcar (lambda (buff)
              (when (bufferp buff)
                (if (buffer-live-p buff)
                    (kill-buffer buff))
                (unless (buffer-live-p buff)
                  (setq consult-gh--open-files-buffers
                        (delete buff consult-gh--open-files-buffers)))))
            consult-gh--open-files-buffers)))

;;;###autoload
(defun consult-gh-refresh-view ()
  "Refresh the buffer viewing a consult-gh topic."
  (interactive nil consult-gh-repo-view-mode consult-gh-issue-view-mode consult-gh-pr-view-mode consult-gh-release-view-mode consult-gh-workflow-view-mode consult-gh-run-view-mode consult-gh-file-view-mode consult-gh-misc-view-mode)
  (consult-gh-with-host
   (consult-gh--auth-account-host)
   (let* ((topic consult-gh--topic)
          (type (get-text-property 0 :type topic))
          (repo (get-text-property 0 :repo topic)))
     (cond
      ((equal type "repo")
       (funcall #'consult-gh--repo-view repo (current-buffer)))
      ((equal type "issue")
       (let* ((number (get-text-property 0 :number topic)))
         (funcall #'consult-gh--issue-view repo number (current-buffer))))
      ((equal type "pr")
       (let* ((number (get-text-property 0 :number topic)))
         (pcase (get-text-property 0 :view topic)
           ("pr"
            (funcall #'consult-gh--pr-view repo number (current-buffer)))
           ("diff"
            (funcall #'consult-gh--pr-view-diff repo number (current-buffer))))))
      ((equal type "release")
       (let* ((tagname (get-text-property 0 :tagname topic)))
         (funcall #'consult-gh--release-view repo tagname (current-buffer))))
      ((equal type "workflow")
       (let* ((workflow-id (get-text-property 0 :id topic)))
         (funcall #'consult-gh--workflow-view repo workflow-id (current-buffer))))
      ((equal type "compareDiff")
       (funcall #'consult-gh-topics--pr-create-view-diff nil t))
      ((equal type "compareCommits")
       (funcall #'consult-gh-topics--pr-create-view-commits nil t))
      ((equal type "compareFileChanges")
       (funcall #'consult-gh-topics--pr-create-view-file-changes nil t))
      ((equal type "file")
       (when (and (buffer-modified-p) (y-or-n-p "This will discard your edits and pull the file from remote?  Do you want to Continue?"))
       (let* ((path (get-text-property 0 :path topic))
              (api-url (get-text-property 0 :api-url topic))
              (ref (get-text-property 0 :ref topic)))
         (funcall #'consult-gh--files-view repo path api-url nil (file-name-directory (buffer-file-name)) nil ref t))))
      ((equal type "commit")
         (let* ((sha (get-text-property 0 :sha topic)))
           (funcall #'consult-gh--commit-view repo sha (current-buffer))))))))

;;;###autoload
(defun consult-gh (&rest args)
  "Convinient wrapper function for favorite interactive command.

Calls the function in `consult-gh-default-interactive-command'
and passes ARGS to it."
  (interactive)
  (apply (or consult-gh-default-interactive-command #'consult-gh-search-repos) args))

;;;###autoload
(defun consult-gh-dired (&optional repo path ref buffer no-select revert)
  "Open consult-gh REPO contents in REF at PATH in a dired-like BUFFER.

Description of Arguments:

  REPO      a string; full name of a repository.
  PATH      a string; path of file in repo to copy.
  REF       a string; the name of a branch or tag in REPO.
  BUFFER    a buffer; the bufffer to open contents in.
  NO-SELECT a boolean; when non-nil do not switch to buffer.
  REVERT    a boolean; when non-nil pull the data from GitHub."
  (interactive)

  (if (and buffer (stringp buffer))
      (setq buffer (get-buffer buffer)))

  (if (and (bufferp buffer)
           (buffer-live-p buffer)
           (> (buffer-size buffer) 0)
           (equal (buffer-name buffer) (format "consult-gh-dired-%s[%s]):%s" repo ref (or path "./")))
           (not revert))
      (progn
        (unless no-select (funcall consult-gh-switch-to-buffer-func buffer))
        buffer)
    (let* ((repo (or repo  (substring-no-properties (get-text-property 0 :repo (consult-gh-search-repos repo t)))))
           (ref (or ref (substring-no-properties (consult-gh--read-ref repo))
                    (or consult-gh-default-branch-to-load "HEAD")))
           (tempdir (expand-file-name (concat repo "/" ref "/")
                                      (or consult-gh--current-tempdir (consult-gh--tempdir))))
           (buffername (format "consult-gh-dired-%s[%s]):%s" repo ref (or path "./")))
           (buffer (or (and (bufferp buffer) buffer)
                       (get-buffer-create buffername)))
           (topic (format "%s/%s" repo path))
           (path (if (equal path "./") nil path))
           (files-list (consult-gh-dired--get-files-list repo path ref))
           (files-list (and (listp files-list)
                            (consult--slow-operation "Collecting Repo Contents ..." (mapcar #'consult-gh-dired--format files-list))))
           (files (consult-gh-dired--files-list-to-string files-list)))
      (unless (file-exists-p tempdir)
        (make-directory tempdir t))
      (when (stringp files)
        (with-current-buffer buffer
         (save-excursion
            (read-only-mode -1)
            (erase-buffer)
            (insert (propertize (concat repo ":" (if (stringp path) (format " ./%s" path))) 'face 'dired-header)
                    "\n\n")
            (if (and (stringp path)
                     (not (equal path "./")))
                (insert (propertize (concat "  ./" (file-name-directory (string-remove-suffix "/" path)) ": ")
                                    'face 'dired-directory
                                    :class "file"
                                    :type "file"
                                    :mode "directory"
                                    :object-type "tree"
                                    :path (or (and (stringp path)
                                                   (file-name-directory (string-remove-suffix "/" path)))
                                              "./")
                                    'help-echo "mouse-2: visit this file in other window"
                                    'mouse-face 'highlight)
                        "\n"))
            (insert files)
            (goto-char (point-min))
            (while (< (point) (point-max))
              (let ((parent (get-text-property (point) :parent)))
                (forward-line)
                (let ((new-parent (get-text-property (point) :parent)))
                  (if (not (equal new-parent parent))
                      (insert "\n")
                    (setq parent new-parent)))))
            (add-text-properties 0 1 (list :repo repo :path path :ref ref :tempdir tempdir) topic)
            (consult-gh-dired-mode)
            (setq-local consult-gh--topic topic)
            (setq-local default-directory tempdir)
            (rename-buffer buffername t)
            (set-buffer-modified-p nil)
            (read-only-mode +1))
          (unless no-select (switch-to-buffer (current-buffer)))
          (current-buffer))))))

(defun consult-gh-dired-revert ()
"Revert current `consult-gh-dired-mode' buffer."
(interactive nil consult-gh-dired-mode)
 (if (derived-mode-p 'consult-gh-dired-mode)
  (when (derived-mode-p 'consult-gh-dired-mode)
    (let* ((topic consult-gh--topic)
           (repo (get-text-property 0 :repo topic))
           (path  (get-text-property 0 :path topic))
           (ref  (get-text-property 0 :ref topic))
           (pos (point)))
      (consult-gh-dired repo path ref (current-buffer) nil t)
      (goto-char (min pos (point-max)))
      (recenter-top-bottom)))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-find-file (&optional repo path ref mode no-select find-func)
  "Open the file at path in `consult-gh-dired-mode'.

PATH defaults to the file path named on the current line in
`consult-gh-dired'.

This is used to visit files when in `consult-gh-dired-mode'.

Description of Arguments:
  REPO       a string; full name of a repository.
  PATH       a string; path of the file to copy.
  REF        a string; the name of a branch or tag in REPO.
  MODE       a string; mode of object “file”, “directory”, “symlink”
             or “commit”.
  NO-SELECT  a boolean; when non-nil do not switch to buffer.
  FIND-FUNC  a function; function to use instead of `find-file'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((topic consult-gh--topic)
             (repo (or repo (get-text-property 0 :repo topic)))
             (ref (or ref (get-text-property 0 :ref topic)))
             (tempdir (or (get-text-property 0 :tempdir topic)
                          (expand-file-name
                           (concat repo "/" ref "/")
                           (or consult-gh--current-tempdir (consult-gh--tempdir))
                           )))
             (mode (or mode (get-text-property (point) :mode)))
             (path (or path (get-text-property (point) :path))))
        (unless (file-exists-p tempdir)
           (make-directory (file-name-directory tempdir) t))
(consult-gh-dired--find-file repo path mode ref no-select tempdir find-func))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-find-file-other-window (&optional repo path ref mode no-select _find-func)
  "Open the file at path in other window.

PATH defaults to the file path named on the current line in
`consult-gh-dired'.

For description of PATH, REPO, REF, MODE, and NO-SELECT
see `consult-gh-dired-find-file'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (consult-gh-dired-find-file repo path ref mode no-select #'find-file-other-window)
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-mouse-find-file (event &optional find-func)
  "Visit the file or directory name you click on.

Description of Arguments:
  EVENT      a click, drag, touch screen, or key press event.
  FIND-FUNC  a function; function to use instead of
             `consult-gh-dired-find-file'."
  (interactive "e" consult-gh-dired-mode)
  (let* ((find-func (or find-func #'consult-gh-dired-find-file))
         (window (posn-window (event-end event)))
         (pos (posn-point (event-end event))))
    (if (not (windowp window))
	(error "No file chosen"))
    (with-current-buffer (window-buffer window)
      (save-mark-and-excursion
        (goto-char pos)
        (funcall find-func)))))

(defun consult-gh-dired-mouse-find-file-other-window (event &optional find-func)
  "Visit the file or directory name you click on in other window.

For description of EVENT and FIND-FUNC,
see `consult-gh-dired-mouse-find-file'."
  (interactive "e" consult-gh-dired-mode)
  (consult-gh-dired-mouse-find-file event (or find-func #'consult-gh-dired-find-file-other-window)))

(defun consult-gh-dired-file-view (&optional repo path ref mode _no-select _find-func)
  "Open the file at path in other window.

PATH defaults to the file path named on the current line in
`consult-gh-dired'.

For description of PATH, REPO, REF, and MODE
see `consult-gh-dired-find-file'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (view-buffer (consult-gh-dired-find-file repo path ref mode t))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-delete-file ()
  "Delete marked files or the file at point."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((topic consult-gh--topic)
             (repo (get-text-property 0 :repo topic))
             (ref (get-text-property 0 :ref topic))
             (paths (list)))

        (when (stringp ref)
          (cond
           ((equal (get-text-property 0 :type ref) "sha")
            (user-error "Cannot delete files in a commit ref"))
           ((equal (get-text-property 0 :type ref) "tag")
            (user-error "Cannot delete files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))

        (setq paths (or (consult-gh--dired-map-over-marks (lambda () (get-text-property (point) :path)))
                        (and (region-active-p)
                             (save-mark-and-excursion
                               (let* ((start (region-beginning))
                                      (end (region-end)))
                                 (goto-char start)
                                 (while (< (point) end)
                                   (setq paths (append paths (list (get-text-property (line-beginning-position) :path))))
                                   (if (< (line-end-position) end)
                                       (forward-line +1)
                                     (goto-char end)))
                                 paths)))
                        (list (get-text-property (point) :path))))
        (when (listp paths)
          (setq paths (remove nil (cl-remove-duplicates paths :test #'equal))))
        (consult-gh--files-delete repo paths ref))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-create-file ()
  "Create a new file in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((topic consult-gh--topic)
             (repo (get-text-property 0 :repo topic))
             (ref (get-text-property 0 :ref topic))
             (_ (when (stringp ref)
                  (cond
                   ((equal (get-text-property 0 :type ref) "sha")
                    (user-error "Cannot create files in a commit ref"))
                   ((equal (get-text-property 0 :type ref) "tag")
                    (user-error "Cannot create files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'")))))
             (path (get-text-property (point) :path))
             (dir (and (stringp path) (file-name-directory path)))
             (dir (if dir (file-name-as-directory dir)))
             (dir-name (or dir "./"))
             (file-name (read-string (concat "File Name: "
                                             (if (stringp path)
                                                 dir-name))))
             (new-path (concat dir file-name))
             (file (format "%s/%s" repo path)))


        (add-text-properties 0 1 (list :repo repo
                                       :type "file"
                                       :mode "file"
                                       :path new-path
                                       :ref ref
                                       :title nil
                                       :api-url nil
                                       :changed-locally nil
                                       :new t
                                       :object-type "blob")
                             file)
        (consult-gh-create-file file))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-copy-file ()
  "Copy file(s) in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (when (equal (get-text-property (point) :type) "file")
        (let* ((topic consult-gh--topic)
               (repo (get-text-property 0 :repo topic))
               (ref (get-text-property 0 :ref topic))
               (files (list)))

          (when (stringp ref)
            (cond
             ((equal (get-text-property 0 :type ref) "sha")
              (user-error "Cannot copy files in a commit ref"))
             ((equal (get-text-property 0 :type ref) "tag")
              (user-error "Cannot copy files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))

          (setq files (or (consult-gh--dired-map-over-marks (lambda () (text-properties-at (point))))
                          (and (region-active-p)
                               (save-mark-and-excursion
                                 (let* ((start (region-beginning))
                                        (end (region-end)))
                                   (goto-char start)
                                   (while (< (point) end)
                                     (setq files (append files (list (text-properties-at (point)))))
                                     (if (< (line-end-position) end)
                                         (forward-line +1)
                                       (goto-char end)))
                                   files)))
                          (list (text-properties-at (point)))))

          (cond
           ((length= files 1)
            (let* ((file (car files))
                   (path (plist-get file :path))
                   (parent (plist-get file :parent))
                   (old-file-name (and (stringp path)
                                       (file-name-nondirectory path)))
                   (new-path (consult-gh--files-read-file repo ref (if (stringp parent)
                                                                       parent
                                                                     nil)
                                                          old-file-name "Enter New File Path: " nil nil nil nil))
                   (tries 0)
                   (new-path (if (and (stringp new-path)
                                      (get-text-property 0 :existing new-path))
                                 (while (< tries 3)
                                   (cl-incf tries)
                                   (consult-gh--files-read-file repo ref (if (stringp parent)
                                                                             parent
                                                                           nil)
                                                                old-file-name "Enter New File Path: " nil nil nil nil))
                               new-path))
                   (new-path (and (stringp new-path)
                                  (not (string-empty-p new-path))
                                  (get-text-property 0 :path new-path)))
                   (view-buffer (consult-gh-dired-find-file repo path ref "file" t))
                   (content (with-current-buffer view-buffer
                              (widen)
                              (buffer-substring (point-min) (point-max)))))
              (when (stringp new-path)
                (add-text-properties 0 1 (list :repo repo
                                               :path new-path
                                               :api-url nil
                                               :mode "file"
                                               :size nil
                                               :ref ref
                                               :sha nil
                                               :class "file"
                                               :type "file"
                                               :object-type "blob"
                                               :content content
                                               :view-buffer view-buffer
                                               :new t)
                                     new-path))
              (consult-gh--create-commit (list new-path) repo ref nil)))

           ((length> files 1)
            (let* ((new-dir (consult--read (mapcar #'consult-gh--file-format (append (list (list "." :repo repo :ref ref :api-url nil :path nil :size nil :object-type "tree" :type "directory" :mode "directory" :new nil))
                                                                                     (consult-gh--files-directory-items repo nil ref nil)))
                                           :prompt "Select Directory: "
                                           :lookup #'consult-gh--file-lookup))
                   (files-list (remove nil (cl-loop for file in files
                                                    collect
                                                    (let* ((path (plist-get file :path))
                                                           (old-file-name (and (stringp path)
                                                                               (file-name-nondirectory path)))
                                                           (new-path (and (stringp path)
                                                                          (stringp new-dir)
                                                                          (concat  (file-name-as-directory new-dir) old-file-name)))
                                                           (view-buffer (consult-gh-dired-find-file repo path ref "file" t))
                                                           (content (with-current-buffer view-buffer
                                                                      (widen)
                                                                      (buffer-substring-no-properties (point-min) (point-max)))))
                                                      (when (stringp new-path)

                                                        (add-text-properties 0 1 (list :repo repo
                                                                                       :path new-path
                                                                                       :api-url nil
                                                                                       :mode "file"
                                                                                       :size nil
                                                                                       :ref ref
                                                                                       :sha nil
                                                                                       :class "file"
                                                                                       :type "file"
                                                                                       :object-type "blob"
                                                                                       :content content
                                                                                       :view-buffer view-buffer
                                                                                       :new t)
                                                                             new-path)
                                                        new-path))))))
              (consult-gh--create-commit files-list repo ref nil))))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-rename-file ()
  "Rename or Move file(s) in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((topic consult-gh--topic)
             (repo (get-text-property 0 :repo topic))
             (ref (get-text-property 0 :ref topic))
             (files (list)))

        (when (stringp ref)
          (cond
           ((equal (get-text-property 0 :type ref) "sha")
            (user-error "Cannot rename files in a commit ref"))
           ((equal (get-text-property 0 :type ref) "tag")
            (user-error "Cannot rename files in a tag ref.  If this is a release tag, you can edit the release using `consult-gh-release-edit'"))))


        (setq files (or (consult-gh--dired-map-over-marks (lambda () (text-properties-at (point))))
                        (and (region-active-p)
                             (save-mark-and-excursion
                               (let* ((start (region-beginning))
                                      (end (region-end)))
                                 (goto-char start)
                                 (while (< (point) end)
                                   (setq files (append files (list (text-properties-at (point)))))
                                   (if (< (line-end-position) end)
                                       (forward-line +1)
                                     (goto-char end)))
                                 files)))
                        (list (text-properties-at (point)))))

        (cond
         ((and (length= files 1)
               (plist-get (car files) :type))
          (let* ((file (car files))
                 (path (plist-get file :path))
                 (mode (plist-get file :mode))
                 (object-type (plist-get file :object-type))
                 (parent (plist-get file :parent))
                 (parent (if (equal mode "directory")
                             (and (stringp parent)
                                  (file-name-directory (string-remove-suffix "/" parent)))
                           parent))
                 (old-file-name (and (stringp path)
                                     (file-name-nondirectory path)))
                 (new-file (consult-gh--files-read-file repo ref (if (stringp parent)
                                                                     parent
                                                                   nil)
                                                        old-file-name "Enter New File Path: " nil nil nil nil))
                 (new-file (if (and (stringp new-file)
                                    (get-text-property 0 :existing new-file))
                               (pcase (consult--read (list
                                                      (cons "Choose a different path/name" :reselect)
                                                      (cons "Update the existing file" :update))
                                                     :prompt "A file with that name already exists.  What do you want to do?"
                                                     :lookup #'consult--lookup-cdr)
                                 (':reselect (consult-gh--files-read-file repo ref (file-name-directory (get-text-property 0 :path new-file))
                                                                          (file-name-nondirectory (get-text-property 0 :path new-file)) "Enter New File Path: " nil nil nil nil))
                                 (':update new-file))
                             new-file))
                 (new-path (and (stringp new-file)
                                (not (string-empty-p new-file))
                                (get-text-property 0 :path new-file)))
                 (view-buffer (if (or (equal mode "file")
                                      (equal mode "symlink"))
                                  (consult-gh--files-view repo path nil t nil nil ref)))
                 (content (and (bufferp view-buffer)
                               (buffer-live-p view-buffer)
                               (with-current-buffer view-buffer
                                 (widen)
                                 (buffer-substring-no-properties (point-min) (point-max))))))

            (when (stringp new-path)
              (add-text-properties 0 1 (list :repo repo
                                             :old-path (substring-no-properties path)
                                             :new-path (substring-no-properties new-path)
                                             :api-url nil
                                             :mode mode
                                             :size nil
                                             :ref ref
                                             :sha nil
                                             :class "file"
                                             :type "file"
                                             :object-type object-type
                                             :content content
                                             :view-buffer view-buffer
                                             :new t)
                                   new-path))
            (if (not (equal new-path path))
                (consult-gh--rename-commit (list new-path) repo ref)
              (message "%s" (propertize "The new file path is the same as old one. Nothing to change!" 'face 'warning)))))

         ((length> files 1)
          (let* ((new-dir (consult--read (mapcar #'consult-gh--file-format (append (list (list "." :repo repo :ref ref :api-url nil :path nil :size nil :object-type "tree" :type "directory" :mode "directory" :new nil))
                                                                                   (consult-gh--files-directory-items repo nil ref nil)))
                                         :prompt "Move file(s) to: "
                                         :lookup #'consult-gh--file-lookup))
                 (files-list (remove nil (cl-loop for file in files
                                                  collect
                                                  (if (or (equal (plist-get file :mode) "file")
                                                          (equal (plist-get file :mode) "symlink"))
                                                      (let* ((path (plist-get file :path))
                                                             (mode (plist-get file :mode))
                                                             (object-type (plist-get file :object-type))
                                                             (old-file-name (and (stringp path)
                                                                                 (file-name-nondirectory path)))
                                                             (new-path (and (stringp path)
                                                                            (stringp new-dir)
                                                                            (concat  (file-name-as-directory new-dir) old-file-name)))
                                                             (view-buffer (if (or (equal mode "file")
                                                                                  (equal mode "symlink"))
                                                                              (consult-gh--files-view repo path nil t nil nil ref)))
                                                             (content (and (bufferp view-buffer)
                                                                           (buffer-live-p view-buffer)
                                                                           (with-current-buffer view-buffer
                                                                             (widen)
                                                                             (buffer-substring-no-properties (point-min) (point-max))))))
                                                        (when (stringp new-path)
                                                          (add-text-properties 0 1 (list :repo repo
                                                                                         :old-path path
                                                                                         :new-path new-path
                                                                                         :api-url nil
                                                                                         :mode mode
                                                                                         :size nil
                                                                                         :ref ref
                                                                                         :sha nil
                                                                                         :class "file"
                                                                                         :type "file"
                                                                                         :object-type object-type
                                                                                         :content content
                                                                                         :view-buffer view-buffer
                                                                                         :new t)
                                                                               new-path)
                                                          (if (not (equal new-path path))
                                                              new-path
                                                            (message "Skipping %s because %s" (propertize path 'face 'consult-gh-date) (propertize "The new file path is the same as old one for this file!" 'face 'warning))))))))))
            (consult-gh--rename-commit files-list repo ref)))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-save-file ()
  "Save file(s) in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((topic consult-gh--topic)
             (repo (get-text-property 0 :repo topic))
             (ref (get-text-property 0 :ref topic))
             (targetdir (or consult-gh-default-save-directory default-directory))
             (files (list)))

        (setq files (or (consult-gh--dired-map-over-marks (lambda () (text-properties-at (point))))
                        (and (region-active-p)
                             (save-mark-and-excursion
                               (let* ((start (region-beginning))
                                      (end (region-end)))
                                 (goto-char start)
                                 (while (< (point) end)
                                   (setq files (append files (list (text-properties-at (point)))))
                                   (if (< (line-end-position) end)
                                       (forward-line +1)
                                     (goto-char end)))
                                 files)))
                        (list (text-properties-at (point)))))
        (when files
          (cond
           ((and (length= files 1)
                 (plist-get (car files) :type))
            (let* ((file (car files))
                   (path (plist-get file :path))
                   (mode (plist-get file :mode))
                   (filename (and (stringp path) (file-name-nondirectory path)))
                   (save-path (pcase mode
                                ("directory"
                                 (consult-gh--read-local-file nil "Save in Directory: " (and (stringp targetdir) (file-name-as-directory targetdir)) nil nil))
                                (_
                                 (consult-gh--read-local-file nil "Enter File Save Path: " (concat (and (stringp targetdir) (file-name-as-directory targetdir)) filename) nil nil))))
                   (save-path (if (and (equal mode "file")
                                           (stringp save-path)
                                           (file-directory-p save-path))
                                      (expand-file-name filename save-path)
                                    (expand-file-name save-path))))
              (when (and repo path save-path)
                (consult-gh-dired--save-file repo path mode save-path ref))))
         ((length> files 1)
          (let* ((save-dir (consult-gh--read-local-file nil "Save in Directory: " (and (stringp targetdir) (file-name-as-directory targetdir)) nil nil)))
          (mapc (lambda (f)
                  (when (plistp f)
                    (let* ((path (plist-get f :path))
                           (mode (plist-get f :mode))
                           (save-path (and (stringp path)
                                           (stringp save-dir)
                                           (expand-file-name path save-dir))))
                           (when (and repo path save-path)
                             (consult-gh-dired--save-file repo path mode save-path ref)))))
                files))))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh--dired-goto-prev-directory-header ()
  "Move mark to the previous directory header."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (progn
        (goto-char (line-beginning-position))
        (when (and (equal (get-text-property (line-beginning-position) :mode) "directory")
                   (equal last-command #'consult-gh--dired-goto-prev-directory-header))
          (forward-line -1))
        (while (and (> (point) (point-min))
                    (or (not (equal (get-text-property (line-beginning-position) :mode) "directory"))
                        (equal (get-text-property (point) 'invisible) 'consult-gh-dired)))
          (forward-line -1))
        (if (save-excursion (equal (forward-line -1) 0))
            (progn (goto-char (line-beginning-position))
                   (point))
          (progn
            (message "%s" (propertize "Beginning of buffer!" 'face 'warning))
            nil)))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh--dired-goto-next-directory-header ()
  "Move mark to the next directory header."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (progn
        (when (equal (get-text-property (line-beginning-position) :mode) "directory")
          (forward-line +1))
        (while (and (< (point) (point-max))
                    (or (not (equal (get-text-property (line-beginning-position) :mode) "directory"))
                        (equal (get-text-property (point) 'invisible) 'consult-gh-dired)))
          (forward-line +1))
        (if (save-excursion (equal (forward-line +1) 0))
            (progn (goto-char (line-beginning-position))
                   (point))
          (progn
            (message "%s" (propertize "End of buffer!" 'face 'warning))
            nil)))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-mark ()
  "Mark file in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((inhibit-read-only t)
             (mode (get-text-property (point) :mode)))
        (pcase mode
          ('nil )
          ("directory"
           (let ((parent (get-text-property (point) :parent)))
            (save-excursion
              (forward-line)
              (while (and (< (point) (point-max))
                          (cond
                           ((get-text-property (point) :type)
                            (if (string-prefix-p (or parent "") (or (get-text-property (point) :parent) ""))
                                (progn
                                  (unless (or (consult-gh-dired-line-marked-p)
                                            (equal (get-text-property (line-beginning-position) :mode) "directory"))
                                  (consult-gh--dired-mark-line))
                                  (if (equal (forward-line +1) 0) t nil))))
                           (t
                            (if (equal (forward-line +1) 0) t nil))))))))
          (_
           (unless (consult-gh-dired-line-marked-p)
             (consult-gh--dired-mark-line)
             (forward-line +1)))))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-unmark ()
  "Unmark file in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((inhibit-read-only t)
             (mode (get-text-property (point) :mode)))
        (pcase mode
          ('nil )
          ("directory"
           (let ((parent (get-text-property (point) :parent)))
            (save-excursion
              (forward-line)
              (while (and (< (point) (point-max))
                          (cond
                           ((get-text-property (point) :type)
                            (if (string-prefix-p (or parent "") (or (get-text-property (point) :parent) ""))
                                (progn (consult-gh--dired-unmark-line)
                                       (if (equal (forward-line +1) 0) t nil))))
                           (t
                            (if (equal (forward-line +1) 0) t nil))))))))
          (_
           (consult-gh--dired-unmark-line))))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-toggle-marks (&optional start end)
  "Toggle marked files from START to END in `consult-gh-dired-mode'.

marked files become unmarked, and vice versa.

If START and END are non-nil, toggle marks in the region from
START to END.  If not, but region is active, toggle marks in
active region only.  Otherwise toggle marks in the entire buffer region only."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (let* ((inhibit-read-only t)
             (start (or start
                        (and (region-active-p)
                             (region-beginning))
                        (point-min)))
             (end (or end
                      (and (region-active-p)
                             (region-end))
                        (point-max))))
        (save-excursion
          (goto-char start)
          (while (and (< (point) end)
                      (cond
                       ((get-text-property (point) :type)
                        (unless (equal (get-text-property (point) :mode) "directory")
                          (consult-gh--dired-toggle-mark))
                        (if (equal (forward-line +1) 0) t nil))
                      (t
                       (if (equal (forward-line +1) 0) t nil)))))))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-unmark-all-marks ()
  "Unmark all marked files in `consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (consult-gh--dired-map-over-marks #'consult-gh-dired-unmark)
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-up-directory ()
  "Open parent directory of the current directory."
  (interactive nil consult-gh-dired-mode)
 (if (derived-mode-p 'consult-gh-dired-mode)
  (let* ((topic consult-gh--topic)
         (repo (and (stringp topic) (get-text-property 0 :repo topic)))
         (ref (and (stringp topic) (get-text-property 0 :ref topic)))
         (path (and (stringp topic) (get-text-property 0 :path topic)))
         (dir (and (stringp path)
                  (file-name-directory (string-remove-suffix "/" path)))))
(if path
  (consult-gh-dired repo dir ref (and consult-gh-files-reuse-dired-like-buffer (current-buffer)))
  (message "No parent doirectory!")))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-next-marked-file ()
  "Find the position of next marked line."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (progn
        (when (consult-gh-dired-line-marked-p)
          (goto-char (line-end-position)))
        (re-search-forward "^\*\s*?[^ \n]" nil t))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-prev-marked-file ()
  "Find the position of previous marked line."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (progn
        (when (consult-gh-dired-line-marked-p)
          (goto-char (line-beginning-position)))
        (re-search-backward "^\*\s*?[^ \n]" nil t))
     (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-hide-subdir ()
  "Hide or unhide the current subdirectory."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (save-excursion
        (unless (equal (get-text-property (point) :mode) "directory")
          (consult-gh--dired-goto-prev-directory-header))
        (let* ((inhibit-read-only t)
               (parent (get-text-property (line-beginning-position) :parent))
               (hidden (save-excursion (goto-char (min (+ (line-end-position) 1) (point-max)))
                                       (consult-gh-dired--hidden-p)))
               (start (line-beginning-position))
               (end))
          (while (and (< (point) (point-max))
                      (cond
                       ((get-text-property (point) :type)
                        (if (string-prefix-p (or parent "") (or (get-text-property (point) :parent) ""))
                            (progn
                              (setq end (line-end-position))
                              (if (equal (forward-line +1) 0) t nil))))
                       (t
                        (if (equal (forward-line +1) 0) t nil)))))
          (if hidden
              (consult-gh--dired-unhide start end)
            (consult-gh--dired-hide start end))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-hide-all ()
  "Hide all subdirectories, leaving only their header lines.
If there is already something hidden, make everything visible again."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (save-excursion
        (with-silent-modifications
          (if (text-property-any (point-min) (point-max) 'invisible 'consult-gh-dired)
	      (consult-gh--dired-unhide (point-min) (point-max))
            ;; hide
            (save-excursion
              (goto-char (point-min))
              (while (and (< (point) (point-max))
                          (consult-gh--dired-goto-next-directory-header))
                (consult-gh-dired-hide-subdir))))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-fold-cycle ()
  "Cycle between showing everything, directories, or root dir."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
        (with-silent-modifications
          (pcase consult-gh--dired-fold-cycle
            ('all
             (consult-gh--dired-fold-show-directories)
             (setq-local consult-gh--dired-fold-cycle 'directory))
            ('directory
             (consult-gh--dired-fold-show-root-only)
             (setq-local consult-gh--dired-fold-cycle 'root))
            ('root
             (consult-gh--dired-fold-show-all)
             (setq-local consult-gh--dired-fold-cycle 'all))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-undo ()
  "Undo for consult-gh-dired-mode'."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (consult-gh--dired-undo)
    (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-next-line (&optional arg)
  "Move cursor vertically down ARG lines in `consult-gh-dired-mode'."
  (interactive "^p" consult-gh-dired-mode)
   (if (derived-mode-p 'consult-gh-dired-mode)
  (consult-gh--dired-next-line arg)
  (message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-previous-line (&optional arg)
  "Move cursor vertically up ARG lines in `consult-gh-dired-mode'."
  (interactive "^p" consult-gh-dired-mode)
   (if (derived-mode-p 'consult-gh-dired-mode)
  (consult-gh--dired-next-line (- (or arg 1)))
(message "Not in a `consult-gh-dired-mode' buffer!")))

(defun consult-gh-dired-hide-details ()
  "Hide details of files."
  (interactive nil consult-gh-dired-mode)
  (if (derived-mode-p 'consult-gh-dired-mode)
      (save-excursion
        (let* ((inhibit-read-only t))
          (goto-char (point-min))
          (if (text-property-any (point-min) (point-max) 'invisible 'consult-gh-dired-details)
              (consult-gh--unhide-region-with-prop 'consult-gh-dired-details nil nil nil)
            (consult-gh--hide-region-with-prop 'consult-gh-dired-details nil nil nil 'consult-gh-dired-details))))
    (message "Not in a `consult-gh-dired-mode' buffer!")))

;;; provide `consult-gh' module

(provide 'consult-gh)

;;; consult-gh.el ends here
