Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ Stacky doesn't use any git or Github APIs. It expects `git` and `gh` cli command
## Usage
`stacky` stores all information locally, within your git repository
Syntax is as follows:
- `stacky info`: show all stacks , add `-pr` if you want to see GitHub PR numbers (slows things down a bit)
- `stacky info`: show all stacks , add `-pr` if you want to see GitHub PR numbers (slows things down a bit)
- `stacky inbox [--compact]`: show all active GitHub pull requests for the current user, organized by status (waiting on you, waiting on review, approved, and PRs awaiting your review). Use `--compact` or `-c` for a condensed one-line-per-PR view with clickable PR numbers.
- `stacky branch`: per branch commands (shortcut: `stacky b`)
- `stacky branch up` (`stacky b u`): move down the stack (towards `master`)
- `stacky branch down` (`stacky b d`): move down the stack (towards `master`)
- `stacky branch down` (`stacky b d`): move down the stack (towards `master`)
- `stacky branch new <name>`: create a new branch on top of the current one
- `stacky commit [-m <message>] [--amend] [--allow-empty]`: wrapper around `git commit` that syncs everything upstack
- `stacky branch commit <name> [-m <message>] [-a]`: create a new branch and commit changes in one command
- `stacky commit [-m <message>] [--amend] [--allow-empty] [-a]`: wrapper around `git commit` that syncs everything upstack
- `stacky amend`: will amend currently tracked changes to top commit
- Based on the first argument (`stack` vs `upstack` vs `downstack`), the following commands operate on the entire current stack, everything upstack from the current PR (inclusive), or everything downstack from the current PR:
- `stacky stack info [--pr]`
Expand All @@ -56,12 +58,12 @@ The indicators (`*`, `~`, `!`) mean:
```
$ stacky --help
usage: stacky [-h] [--color {always,auto,never}]
{continue,info,commit,amend,branch,b,stack,s,upstack,us,downstack,ds,update,import,adopt,land,push,sync,checkout,co,sco} ...
{continue,info,commit,amend,branch,b,stack,s,upstack,us,downstack,ds,update,import,adopt,land,push,sync,checkout,co,sco,inbox} ...

Handle git stacks

positional arguments:
{continue,info,commit,amend,branch,b,stack,s,upstack,us,downstack,ds,update,import,adopt,land,push,sync,checkout,co,sco}
{continue,info,commit,amend,branch,b,stack,s,upstack,us,downstack,ds,update,import,adopt,land,push,sync,checkout,co,sco,inbox}
continue Continue previously interrupted command
info Stack info
commit Commit
Expand All @@ -71,12 +73,14 @@ positional arguments:
upstack (us) Operations on the current upstack
downstack (ds) Operations on the current downstack
update Update repo
import Import Graphite stack
adopt Adopt one branch
land Land bottom-most PR on current stack
push Alias for downstack push
sync Alias for stack sync
checkout (co) Checkout a branch
sco Checkout a branch in this stack
inbox List all active GitHub pull requests for the current user

optional arguments:
-h, --help show this help message and exit
Expand Down
190 changes: 190 additions & 0 deletions src/stacky/stacky.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,34 @@ def cmd_branch_new(stack: StackBranchSet, args):
run(CmdArgs(["git", "update-ref", "refs/stack-parent/{}".format(name), b.commit, ""]))


def cmd_branch_commit(stack: StackBranchSet, args):
"""Create a new branch and commit all changes with the provided message"""
global CURRENT_BRANCH

# First create the new branch (same logic as cmd_branch_new)
b = stack.stack[CURRENT_BRANCH]
assert b.commit
name = args.name
create_branch(name)
run(CmdArgs(["git", "update-ref", "refs/stack-parent/{}".format(name), b.commit, ""]))

# Update global CURRENT_BRANCH since we just checked out the new branch
CURRENT_BRANCH = BranchName(name)

# Reload the stack to include the new branch
load_stack_for_given_branch(stack, CURRENT_BRANCH)

# Now commit all changes with the provided message (or open editor if no message)
do_commit(
stack,
message=args.message,
amend=False,
allow_empty=False,
edit=True,
add_all=args.add_all,
)


def cmd_branch_checkout(stack: StackBranchSet, args):
branch_name = args.name
if branch_name is None:
Expand Down Expand Up @@ -1660,6 +1688,157 @@ def cmd_land(stack: StackBranchSet, args):
cout("\n✓ Success! Run `stacky update` to update local state.\n", fg="green")


def cmd_inbox(stack: StackBranchSet, args):
"""List all active GitHub pull requests for the current user"""
fields = [
"number",
"title",
"headRefName",
"baseRefName",
"state",
"url",
"createdAt",
"updatedAt",
"author",
"reviewDecision",
"reviewRequests"
]

# Get all open PRs authored by the current user
my_prs_data = json.loads(
run_always_return(
CmdArgs(
[
"gh",
"pr",
"list",
"--json",
",".join(fields),
"--state",
"open",
"--author",
"@me"
]
)
)
)

# Get all open PRs where current user is requested as reviewer
review_prs_data = json.loads(
run_always_return(
CmdArgs(
[
"gh",
"pr",
"list",
"--json",
",".join(fields),
"--state",
"open",
"--search",
"review-requested:@me"
]
)
)
)

# Categorize my PRs based on review status
waiting_on_me = []
waiting_on_review = []
approved = []

for pr in my_prs_data:
if pr["reviewDecision"] == "APPROVED":
approved.append(pr)
elif pr["reviewRequests"] and len(pr["reviewRequests"]) > 0:
waiting_on_review.append(pr)
else:
# No pending review requests, likely needs changes or author action
waiting_on_me.append(pr)

# Sort all lists by updatedAt in descending order (most recent first)
waiting_on_me.sort(key=lambda pr: pr["updatedAt"], reverse=True)
waiting_on_review.sort(key=lambda pr: pr["updatedAt"], reverse=True)
approved.sort(key=lambda pr: pr["updatedAt"], reverse=True)
review_prs_data.sort(key=lambda pr: pr["updatedAt"], reverse=True)

def display_pr_list(prs, color="white"):
for pr in prs:
if args.compact:
# Compact format with only PR number clickable: "#123 Title (branch) Updated: date"
# Create clickable link for just the PR number
pr_number_text = f"#{pr['number']}"
clickable_number = f"\033]8;;{pr['url']}\033\\\033[96m{pr_number_text}\033[0m\033]8;;\033\\"
cout("{} ", clickable_number)
cout("{} ", pr["title"], fg="white")
cout("({}) ", pr["headRefName"], fg="gray")
cout("Updated: {}\n", pr["updatedAt"][:10], fg="gray")
else:
# Full format with clickable PR number
pr_number_text = f"#{pr['number']}"
clickable_number = f"\033]8;;{pr['url']}\033\\\033[96m{pr_number_text}\033[0m\033]8;;\033\\"
cout("{} ", clickable_number)
cout("{}\n", pr["title"], fg=color)
cout(" {} -> {}\n", pr["headRefName"], pr["baseRefName"], fg="gray")
cout(" {}\n", pr["url"], fg="blue")
cout(" Updated: {}, Created: {}\n\n", pr["updatedAt"][:10], pr["createdAt"][:10], fg="gray")

# Display categorized authored PRs
if waiting_on_me:
cout("Your PRs - Waiting on You:\n", fg="red")
display_pr_list(waiting_on_me, "white")
if args.compact:
cout("\n")
else:
cout("\n")

if waiting_on_review:
cout("Your PRs - Waiting on Review:\n", fg="yellow")
display_pr_list(waiting_on_review, "white")
if args.compact:
cout("\n")
else:
cout("\n")

if approved:
cout("Your PRs - Approved:\n", fg="green")
display_pr_list(approved, "white")
if args.compact:
cout("\n")
else:
cout("\n")

if not my_prs_data:
cout("No active pull requests authored by you.\n", fg="green")

# Display PRs waiting for review
if review_prs_data:
cout("Pull Requests Awaiting Your Review:\n", fg="yellow")
for pr in review_prs_data:
if args.compact:
# Compact format with only PR number clickable: "#123 Title (branch) Updated: date"
# Create clickable link for just the PR number
pr_number_text = f"#{pr['number']}"
clickable_number = f"\033]8;;{pr['url']}\033\\\033[96m{pr_number_text}\033[0m\033]8;;\033\\"
cout("{} ", clickable_number)
cout("{} ", pr["title"], fg="white")
cout("({}) ", pr["headRefName"], fg="gray")
cout("by {} ", pr["author"]["login"], fg="gray")
cout("Updated: {}\n", pr["updatedAt"][:10], fg="gray")
else:
# Full format with clickable PR number
pr_number_text = f"#{pr['number']}"
clickable_number = f"\033]8;;{pr['url']}\033\\\033[96m{pr_number_text}\033[0m\033]8;;\033\\"
cout("{} ", clickable_number)
cout("{}\n", pr["title"], fg="white")
cout(" {} -> {}\n", pr["headRefName"], pr["baseRefName"], fg="gray")
cout(" Author: {}\n", pr["author"]["login"], fg="gray")
cout(" {}\n", pr["url"], fg="blue")
cout(" Updated: {}, Created: {}\n\n", pr["updatedAt"][:10], pr["createdAt"][:10], fg="gray")
else:
cout("No pull requests awaiting your review.\n", fg="yellow")


def main():
logging.basicConfig(format=_LOGGING_FORMAT, level=logging.INFO)
try:
Expand Down Expand Up @@ -1726,6 +1905,12 @@ def main():
branch_new_parser.add_argument("name", help="Branch name")
branch_new_parser.set_defaults(func=cmd_branch_new)

branch_commit_parser = branch_subparsers.add_parser("commit", help="Create a new branch and commit all changes")
branch_commit_parser.add_argument("name", help="Branch name")
branch_commit_parser.add_argument("-m", help="Commit message", dest="message")
branch_commit_parser.add_argument("-a", action="store_true", help="Add all files to commit", dest="add_all")
branch_commit_parser.set_defaults(func=cmd_branch_commit)

branch_checkout_parser = branch_subparsers.add_parser("checkout", aliases=["co"], help="Checkout a branch")
branch_checkout_parser.add_argument("name", help="Branch name", nargs="?")
branch_checkout_parser.set_defaults(func=cmd_branch_checkout)
Expand Down Expand Up @@ -1838,6 +2023,11 @@ def main():
checkout_parser = subparsers.add_parser("sco", help="Checkout a branch in this stack")
checkout_parser.set_defaults(func=cmd_stack_checkout)

# inbox
inbox_parser = subparsers.add_parser("inbox", help="List all active GitHub pull requests for the current user")
inbox_parser.add_argument("--compact", "-c", action="store_true", help="Show compact view")
inbox_parser.set_defaults(func=cmd_inbox)

args = parser.parse_args()
logging.basicConfig(format=_LOGGING_FORMAT, level=LOGLEVELS[args.log_level], force=True)

Expand Down