Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.org
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Release history

** Main branch change
- Feat: Add current-branch PR creation option

** 1.68
- UX: TDD Harness loop externalize the long prompt
Expand Down
97 changes: 87 additions & 10 deletions ai-code-git.el
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
(require 'ai-code-prompt-mode)

(declare-function helm-gtags-create-tags "helm-gtags" (dir &optional label))
(declare-function magit-anything-modified-p "magit" ())
(declare-function magit-branch-p "magit" (branch))
(declare-function magit-branch-read-args "magit-branch" (prompt))
(declare-function magit-call-git "magit-git" (&rest args))
(declare-function magit-diff-visit-directory "magit-diff" (directory))
(declare-function magit-git-lines "magit-git" (&rest args))
(declare-function magit-git-output "magit-git" (&rest args))
(declare-function magit-git-string "magit-git" (&rest args))
(declare-function magit-rev-verify "magit-git" (rev))
(declare-function magit-run-git "magit-git" (&rest args))
(declare-function magit-worktree-status "magit-worktree" ())

(defcustom ai-code-init-project-gtags-label "pygments"
"Default label passed to Helm-Gtags when initializing a project.
Expand All @@ -40,7 +51,7 @@ Candidate values:
(defvar ai-code-files-dir-name)

(defun ai-code--git-ignored-repo-file-p (file root)
"Return non-nil when FILE should be ignored for repo candidates."
"Return non-nil when FILE should be ignored for repo candidates under ROOT."
(when (and file root)
(let ((ignore-dir (file-truename (expand-file-name ai-code-files-dir-name root)))
(truename (file-truename file)))
Expand Down Expand Up @@ -120,7 +131,7 @@ PR Description Steps:
pr-url source-instruction)))

(defun ai-code--build-pr-ci-check-init-prompt (review-source pr-url)
"Build CI checks review prompt for REVIEW-SOURCE with PR-URL."
"Build a CI check review prompt for REVIEW-SOURCE with PR-URL."
(let ((source-instruction
(ai-code--pull-or-review-source-instruction review-source 'review-ci-checks)))
(format "Review GitHub CI checks for pull request: %s
Expand Down Expand Up @@ -168,19 +179,31 @@ CI Checks Review Steps:
(defun ai-code--pull-or-review-pr-with-source (review-source)
"Ask for a target URL and send a prompt for REVIEW-SOURCE to AI."
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai-code--pull-or-review-pr-with-source no longer always asks for a URL (it can branch into current-branch PR creation), but the docstring still says it "Ask[s] for a target URL". Update the docstring to reflect both flows so it stays accurate for callers/readers.

Suggested change
"Ask for a target URL and send a prompt for REVIEW-SOURCE to AI."
"Prompt for an analysis mode and send a prompt for REVIEW-SOURCE to AI.
If the selected mode is `send-current-branch-pr', ask for the target
branch for the current branch PR. Otherwise, ask for the relevant pull
request or issue URL."

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed locally. I updated the docstring to describe both flows: the current-branch PR creation path and the URL-based review/issue path. This will be included in the next PR update.

(let* ((review-mode (ai-code--pull-or-review-pr-mode-choice))
(url-prompt (ai-code--pull-or-review-url-prompt review-mode))
(target-url (ai-code-read-string url-prompt))
(init-prompt (ai-code--build-pr-init-prompt review-source target-url review-mode))
(init-prompt
(if (eq review-mode 'send-current-branch-pr)
(let* ((current-branch (ai-code--require-current-branch))
(default-target-branch
(ai-code--default-pr-target-branch current-branch))
(target-branch
(ai-code-read-string "Target branch to merge into: "
default-target-branch)))
(ai-code--build-send-current-branch-pr-init-prompt
review-source current-branch target-branch))
Comment on lines +184 to +191
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new current-branch PR flow can be invoked even when not in a Git repo. In that case, calling Magit/Git helpers to determine the current branch/target branch is likely to error or produce a confusing failure. Consider explicitly validating repository context (and emitting a clearer user-error) before entering this branch-based flow.

Suggested change
(let* ((current-branch (ai-code--require-current-branch))
(default-target-branch
(ai-code--default-pr-target-branch current-branch))
(target-branch
(ai-code-read-string "Target branch to merge into: "
default-target-branch)))
(ai-code--build-send-current-branch-pr-init-prompt
review-source current-branch target-branch))
(progn
(unless (magit-toplevel)
(user-error "Not inside a Git repository"))
(let* ((current-branch (ai-code--require-current-branch))
(default-target-branch
(ai-code--default-pr-target-branch current-branch))
(target-branch
(ai-code-read-string "Target branch to merge into: "
default-target-branch)))
(ai-code--build-send-current-branch-pr-init-prompt
review-source current-branch target-branch)))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed locally. The current-branch PR path now validates that the command is running inside a Git repository before it tries to resolve branch information, so the failure mode is explicit and user-facing. This will be included in the next PR update.

(let* ((url-prompt (ai-code--pull-or-review-url-prompt review-mode))
(target-url (ai-code-read-string url-prompt)))
(ai-code--build-pr-init-prompt review-source target-url review-mode))))
(prompt (ai-code-read-string "Enter review prompt: " init-prompt)))
(ai-code--insert-prompt prompt)))
Comment on lines +194 to 196
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the user chooses "Send out PR for current branch", the minibuffer prompt still says "Enter review prompt". Consider changing this prompt (conditionally) to something neutral like "Enter prompt" or "Enter PR creation prompt" to avoid misleading wording in the new flow.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed locally. The current-branch PR path now uses a dedicated minibuffer label (Enter PR creation prompt:) instead of the review-specific prompt text. This will be included in the next PR update.


(defun ai-code--pull-or-review-pr-mode-choice ()
"Prompt user to choose analysis mode for a pull request or issue."
;; DONE: add a choice: send out PR for current branch. The feature will ask user the target branch to merge. By default, it should be parent branch of current branch. AI should send out PR with description. The description should looks like it's written by the author, and it should be short.
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s a leftover implementation note (;; DONE: ...) inside ai-code--pull-or-review-pr-mode-choice. This kind of comment becomes stale quickly and adds noise to the interactive path; please remove it (or convert it into a proper issue/commit message) before merging.

Suggested change
;; DONE: add a choice: send out PR for current branch. The feature will ask user the target branch to merge. By default, it should be parent branch of current branch. AI should send out PR with description. The description should looks like it's written by the author, and it should be short.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping this comment intentionally. The original implementation requirement for this task explicitly asked to preserve the existing comment in place and change it to a DONE-prefixed form after implementing the feature, so removing it here would conflict with that requirement.

(let* ((review-mode-alist '(("Review the PR" . review-pr)
("Check unresolved feedback" . check-feedback)
("Investigate issue" . investigate-issue)
("Review GitHub CI checks" . review-ci-checks)
("Prepare PR description" . prepare-pr-description)))
("Prepare PR description" . prepare-pr-description)
("Send out PR for current branch" . send-current-branch-pr)))
(review-mode (completing-read "Select analysis mode (PR or issue): "
review-mode-alist
nil t nil nil "Review the PR")))
Expand All @@ -202,6 +225,59 @@ CI Checks Review Steps:
(_
(ai-code--build-pr-review-init-prompt review-source target-url))))

(defun ai-code--require-current-branch ()
"Return the current branch name or signal a user error."
(or (magit-get-current-branch)
(user-error "Current branch is not available")))

(defun ai-code--normalize-branch-name (branch)
"Normalize BRANCH for display and user defaults."
(when branch
(replace-regexp-in-string
"\\`refs/heads/\\|\\`refs/remotes/[^/]+/\\|\\`origin/"
""
branch)))

(defun ai-code--default-pr-target-branch (current-branch)
"Return the default PR target branch for CURRENT-BRANCH."
(let* ((upstream-branch
(ignore-errors
(magit-git-string "rev-parse"
"--abbrev-ref"
"--symbolic-full-name"
"@{upstream}")))
(normalized-upstream
(ai-code--normalize-branch-name upstream-branch)))
(cond
((and normalized-upstream
(not (string-empty-p normalized-upstream))
(not (string= normalized-upstream current-branch)))
normalized-upstream)
((or (magit-branch-p "main") (magit-branch-p "origin/main"))
"main")
((or (magit-branch-p "master") (magit-branch-p "origin/master"))
"master")
(t "main"))))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid hardcoding main as fallback PR target

When ai-code--default-pr-target-branch cannot infer an upstream and neither main nor master exists, it still returns "main". In repositories whose integration branch is named differently (for example develop or trunk), accepting this default causes the generated PR-creation flow to target a non-existent branch, which can make the downstream gh pr create step fail. The fallback should be derived from an actual existing default branch (e.g., remote HEAD/default branch) instead of an unconditional literal.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed locally. The fallback logic now checks origin/HEAD first and uses the repository's actual remote default branch when upstream is unavailable, instead of unconditionally falling back to "main". This will be included in the next PR update.

Comment on lines +241 to +260
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai-code--default-pr-target-branch is new and contains non-trivial branching (upstream parsing, normalization, main/master fallback), but there are no tests covering these behaviors. Add focused ERT tests that mock magit-git-string/magit-branch-p to validate upstream-derived defaults and the fallback selection logic.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed locally. I added focused coverage for the new default-target-branch behavior, including the remote default branch fallback path and the current-branch PR prompt flow. This will be included in the next PR update.


(defun ai-code--build-send-current-branch-pr-init-prompt (review-source current-branch target-branch)
"Build a PR creation prompt for REVIEW-SOURCE, CURRENT-BRANCH, and TARGET-BRANCH."
(let ((source-instruction
(ai-code--pull-or-review-source-instruction review-source)))
Comment on lines +262 to +265
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR creation prompt reuses ai-code--pull-or-review-source-instruction’s default (review) instruction, which currently tells the backend to "fetch pull request details and review comments". For a PR-creation flow, this instruction is misleading (especially before the PR exists). Add a dedicated review-mode case (e.g., send-current-branch-pr) with backend-specific instructions for creating the PR (gh: gh pr create, MCP: create PR via API), and pass that mode here.

Suggested change
(defun ai-code--build-send-current-branch-pr-init-prompt (review-source current-branch target-branch)
"Build a PR creation prompt for REVIEW-SOURCE, CURRENT-BRANCH, and TARGET-BRANCH."
(let ((source-instruction
(ai-code--pull-or-review-source-instruction review-source)))
(defun ai-code--send-current-branch-pr-source-instruction (review-source)
"Return PR-creation instructions for REVIEW-SOURCE."
(pcase review-source
((or 'gh 'github-cli 'github)
(concat
"Use GitHub CLI to create the pull request. "
"Run `gh pr create` with the current branch as the head branch, "
"target the requested base branch, and include the final title and body."))
((or 'mcp 'github-mcp)
(concat
"Use the available MCP/API GitHub tools to create the pull request directly. "
"Do not fetch review comments for a PR that does not exist yet; "
"create the PR first, then return the resulting PR URL."))
(_
(concat
"Create the pull request using the backend's PR creation capability. "
"Do not treat this as a PR review flow and do not fetch review comments "
"before the PR exists."))))
(defun ai-code--build-send-current-branch-pr-init-prompt (review-source current-branch target-branch)
"Build a PR creation prompt for REVIEW-SOURCE, CURRENT-BRANCH, and TARGET-BRANCH."
(let ((source-instruction
(ai-code--send-current-branch-pr-source-instruction review-source)))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed locally. I added a dedicated source-instruction path for current-branch PR creation so this flow no longer reuses review-oriented instructions about fetching PR details or review comments before the PR exists. This will be included in the next PR update.

(format "Create a pull request from branch %s into %s.

%s

PR Creation Steps:
1. Inspect the current branch changes and open or send out a pull request into %s.
2. Write a concise PR description that sounds like it was written by the author, but do not make it too short.
3. Keep the description focused on the problem, the approach, and the most important verification, with enough detail for reviewers to understand the change quickly.
4. Aim for a compact but complete description, roughly a short summary plus 2 to 3 brief supporting paragraphs or bullet points.
5. Return the final PR URL and the exact description that was used."
current-branch
target-branch
source-instruction
target-branch)))

;;;###autoload
(defun ai-code-pull-or-review-diff-file ()
"Review a diff file with AI Code or generate one if not viewing a diff.
Expand Down Expand Up @@ -742,7 +818,7 @@ PREFIX is the prefix argument."

;;;###autoload
(defun ai-code-update-git-ignore ()
"Ensure repository .gitignore contains AI Code related entries.
"Ensure repository .gitignore has AI Code-related entries.
If not inside a Git repository, do nothing."
(interactive)
(let ((git-root (ai-code--git-root)))
Expand Down Expand Up @@ -817,7 +893,7 @@ If BASE-DIR is in a Git repository, use `git ls-files' to enumerate files."

;;;###autoload
(defun ai-code-git-repo-recent-modified-files (prefix)
"Open or insert one of the most recently modified files in the repo or current dir.
"Open or insert a recently modified file in the repo or current dir.
With no PREFIX argument, prompt for a recently modified file and open it
with `find-file'.

Expand Down Expand Up @@ -851,7 +927,7 @@ buffer from which this command was invoked, instead of visiting the file."
;;;###autoload
(defun ai-code-git-worktree-branch (branch start-point)
"Create BRANCH and check it out in a new centralized worktree.
The worktree path is
The worktree path for START-POINT is
`ai-code-git-worktree-root/REPO-NAME/BRANCH'."
(interactive
(magit-branch-read-args "Create and checkout branch"))
Expand All @@ -872,7 +948,8 @@ The worktree path is
(defun ai-code-git-worktree-action (&optional prefix)
"Dispatch worktree action by PREFIX.
Without PREFIX, call `ai-code-git-worktree-branch'.
With PREFIX (for example C-u), call `magit-worktree-status'."
With PREFIX (for example \\[universal-argument]), call
`magit-worktree-status'."
(interactive "P")
(unless (and (stringp ai-code-git-worktree-root)
(> (length ai-code-git-worktree-root) 0))
Expand Down
29 changes: 26 additions & 3 deletions test/test_ai-code-git.el
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
(require 'ai-code-prompt-mode)
(require 'ai-code-discussion)

(declare-function magit-worktree-status "magit-worktree" ())

(defun ai-code-test--gitignore-required-entries ()
"Return the default ignore entries expected from `ai-code-update-git-ignore'."
(list (concat ai-code-files-dir-name "/")
Expand Down Expand Up @@ -70,7 +72,7 @@ GPATH
gitignore-content))

;; Test entries with whitespace
(let ((gitignore-with-whitespace " .projectile
(let ((gitignore-with-whitespace " .projectile
GTAGS
"))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
Expand Down Expand Up @@ -126,7 +128,7 @@ not be added again."

;; Mock ai-code--git-root to return temp-dir
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional dir) temp-dir)))
(lambda (&optional _dir) temp-dir)))
;; Call the function
(ai-code-update-git-ignore))

Expand Down Expand Up @@ -168,7 +170,7 @@ When .gitignore is missing some entries, they should be added."

;; Mock ai-code--git-root to return temp-dir
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional dir) temp-dir)))
(lambda (&optional _dir) temp-dir)))
;; Call the function
(ai-code-update-git-ignore))

Expand Down Expand Up @@ -261,6 +263,27 @@ When .gitignore is missing some entries, they should be added."
(should (eq (ai-code--pull-or-review-pr-mode-choice)
'review-ci-checks))))

(ert-deftest ai-code-test-pull-or-review-pr-mode-choice-send-current-branch-pr ()
"Choosing current branch PR mode should return `send-current-branch-pr'."
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "Send out PR for current branch")))
(should (eq (ai-code--pull-or-review-pr-mode-choice)
'send-current-branch-pr))))

(ert-deftest ai-code-test-build-send-current-branch-pr-init-prompt ()
"Build a concise PR creation prompt for the current branch."
(let ((prompt (ai-code--build-send-current-branch-pr-init-prompt
'gh-cli
"feature/improve-pr-flow"
"main")))
(let ((case-fold-search nil))
(should (string-match-p "Use gh CLI tool" prompt)))
(should (string-match-p "feature/improve-pr-flow" prompt))
(should (string-match-p "main" prompt))
(should (string-match-p "create a pull request" (downcase prompt)))
(should (string-match-p "short" (downcase prompt)))
(should (string-match-p "author" (downcase prompt)))))

(ert-deftest ai-code-test-pull-or-review-diff-file-prepare-pr-description-github-mcp ()
"When choosing PR description mode, prompt should ask AI to draft a PR description."
(pcase-let ((`(,captured-prompt ,diff-called)
Expand Down
Loading