diff --git a/flakes/common/nix_modules/git/default.nix b/flakes/common/nix_modules/git/default.nix index b90012a1..3ed0095e 100644 --- a/flakes/common/nix_modules/git/default.nix +++ b/flakes/common/nix_modules/git/default.nix @@ -7,6 +7,7 @@ with lib; { environment.systemPackages = with pkgs; [ git + gh ]; environment.shellAliases = { @@ -32,5 +33,6 @@ with lib; (builtins.readFile ./link_ignored.func.sh) (builtins.readFile ./branching_setup.func.sh) (builtins.readFile ./gcpropose.func.sh) + (builtins.readFile ./gpr.func.sh) ]; } diff --git a/flakes/common/nix_modules/git/gpr.func.sh b/flakes/common/nix_modules/git/gpr.func.sh new file mode 100644 index 00000000..d83d8a6a --- /dev/null +++ b/flakes/common/nix_modules/git/gpr.func.sh @@ -0,0 +1,445 @@ +# gpr - GitHub PR helper commands +# Usage: gpr [args] +# gpr create - Create a new PR from current branch +# gpr update - Update existing PR description + +gpr() { + local cmd="${1:-}" + shift 2>/dev/null || true + + case "$cmd" in + create) gpr_create "$@" ;; + update) gpr_update "$@" ;; + -h|--help|help|"") + cat < + +Commands: + create Create a new PR from current branch + update Update existing PR description + +Both commands generate a PR description using LLM based on: + - Full diff against base branch + - All commit messages on the branch + - Opens in \$EDITOR for review before submitting +EOF + return 0 + ;; + *) + echo "Unknown command: $cmd" >&2 + echo "Run 'gpr help' for usage." >&2 + return 1 + ;; + esac +} + +_gpr_check_deps() { + if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Not inside a git repository." >&2 + return 1 + fi + + if ! command -v gh >/dev/null 2>&1; then + echo "Missing dependency: gh (GitHub CLI)" >&2 + return 1 + fi + + if ! command -v curl >/dev/null 2>&1; then + echo "Missing dependency: curl" >&2 + return 1 + fi + + if ! command -v jq >/dev/null 2>&1; then + echo "Missing dependency: jq" >&2 + return 1 + fi + + return 0 +} + +_gpr_get_base_branch() { + # Try to get the base branch from git config or default to origin default + local base + base=$(git config --get init.defaultBranch 2>/dev/null || true) + if [ -z "$base" ]; then + base=$(git remote show origin 2>/dev/null | grep "HEAD branch" | sed 's/.*: //' || true) + fi + if [ -z "$base" ]; then + base="main" + fi + echo "$base" +} + +_gpr_get_current_branch() { + git rev-parse --abbrev-ref HEAD +} + +_gpr_generate_description() { + local base_branch="$1" + local existing_description="${2:-}" + + local LITELLM_BASE_URL="http://h001.net.joshuabell.xyz:8094" + local LITELLM_MODEL="azure-gpt-5-mini-2025-08-07" + + local current_branch + current_branch=$(_gpr_get_current_branch) + + # Get diff against base branch + local diff + diff=$(git diff --no-color --no-ext-diff "${base_branch}...HEAD" 2>/dev/null || git diff --no-color --no-ext-diff "${base_branch}..HEAD" 2>/dev/null || true) + + # Truncate diff if too large + local max_diff_chars=15000 + diff=$(printf "%s" "$diff" | head -c "$max_diff_chars") + + # Get commit messages + local commits + commits=$(git log --oneline "${base_branch}..HEAD" 2>/dev/null || true) + + # Get detailed commit messages + local commit_details + commit_details=$(git log --pretty=format:"%h %s%n%b" "${base_branch}..HEAD" 2>/dev/null || true) + + # Get file changes summary + local files_changed + files_changed=$(git diff --stat "${base_branch}...HEAD" 2>/dev/null || git diff --stat "${base_branch}..HEAD" 2>/dev/null || true) + + local prompt + if [ -n "$existing_description" ]; then + prompt=$(cat <&2 + printf "%s\n" "$body" >&2 + return 1 + fi + + local message + message=$(printf "%s" "$body" | jq -r ' + .choices[0].message.content + | if type == "string" then . + elif type == "array" then (map(select(.type=="text") | .text) | join("")) + else "" + end + ' 2>/dev/null || true) + + if [ -z "$message" ] || [ "$message" = "null" ]; then + echo "Failed to parse model response." >&2 + printf "%s\n" "$body" >&2 + return 1 + fi + + printf "%s" "$message" +} + +_gpr_generate_title() { + local base_branch="$1" + + local LITELLM_BASE_URL="http://h001.net.joshuabell.xyz:8094" + local LITELLM_MODEL="azure-gpt-5-mini-2025-08-07" + + local current_branch + current_branch=$(_gpr_get_current_branch) + + # Get commit messages + local commits + commits=$(git log --oneline "${base_branch}..HEAD" 2>/dev/null || true) + + local prompt + prompt=$(cat <&2 + return 1 + fi + + local message + message=$(printf "%s" "$body" | jq -r '.choices[0].message.content' 2>/dev/null || true) + message=$(printf "%s" "$message" | sed -n '1p' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + if [ -z "$message" ] || [ "$message" = "null" ]; then + # Fallback to branch name + echo "$current_branch" + return 0 + fi + + printf "%s" "$message" +} + +gpr_create() { + _gpr_check_deps || return 1 + + local current_branch + current_branch=$(_gpr_get_current_branch) + + local base_branch + base_branch=$(_gpr_get_base_branch) + + # Check we're not on the default branch + if [ "$current_branch" = "$base_branch" ]; then + echo "Cannot create PR from the default branch ($base_branch)." >&2 + echo "Create a feature branch first." >&2 + return 1 + fi + + # Check if PR already exists + local existing_pr + existing_pr=$(gh pr view --json number,url 2>/dev/null || true) + if [ -n "$existing_pr" ]; then + local pr_url + pr_url=$(printf "%s" "$existing_pr" | jq -r '.url') + echo "A PR already exists for this branch: $pr_url" >&2 + echo "Use 'gpr update' to update the description instead." >&2 + return 1 + fi + + # Check if branch is pushed to remote + local remote_branch + remote_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true) + if [ -z "$remote_branch" ]; then + echo "Branch not pushed to remote. Pushing now..." + git push -u origin "$current_branch" || return 1 + fi + + echo "Generating PR description with LLM..." + local description + description=$(_gpr_generate_description "$base_branch") || return 1 + + echo "Generating PR title..." + local title + title=$(_gpr_generate_title "$base_branch") || return 1 + + # Create temp file for editing + local tmpfile + tmpfile=$(mktemp --suffix=.md) + trap "rm -f '$tmpfile'" EXIT + + cat > "$tmpfile" <&2 + return 1 + fi + + # Parse title (first line) and body (after ---) + local final_title final_body + final_title=$(head -n 1 "$tmpfile") + final_body=$(tail -n +3 "$tmpfile") + + if [ -z "$final_title" ]; then + echo "Aborted: no title provided." >&2 + return 1 + fi + + echo "Creating PR..." + gh pr create --title "$final_title" --body "$final_body" --base "$base_branch" +} + +gpr_update() { + _gpr_check_deps || return 1 + + local current_branch + current_branch=$(_gpr_get_current_branch) + + local base_branch + base_branch=$(_gpr_get_base_branch) + + # Check if PR exists + local existing_pr + existing_pr=$(gh pr view --json number,title,body,url 2>/dev/null || true) + if [ -z "$existing_pr" ]; then + echo "No PR exists for this branch." >&2 + echo "Use 'gpr create' to create one first." >&2 + return 1 + fi + + local pr_url pr_title pr_body + pr_url=$(printf "%s" "$existing_pr" | jq -r '.url') + pr_title=$(printf "%s" "$existing_pr" | jq -r '.title') + pr_body=$(printf "%s" "$existing_pr" | jq -r '.body') + + echo "Updating PR: $pr_url" + echo "Generating updated description with LLM..." + + local description + description=$(_gpr_generate_description "$base_branch" "$pr_body") || return 1 + + # Create temp file for editing + local tmpfile + tmpfile=$(mktemp --suffix=.md) + trap "rm -f '$tmpfile'" EXIT + + cat > "$tmpfile" <&2 + return 1 + fi + + # Parse title and body + local final_title final_body + final_title=$(head -n 1 "$tmpfile") + final_body=$(tail -n +3 "$tmpfile") + + if [ -z "$final_title" ]; then + echo "Aborted: no title provided." >&2 + return 1 + fi + + echo "Updating PR..." + gh pr edit --title "$final_title" --body "$final_body" + + echo "PR updated: $pr_url" +} diff --git a/hosts/lio/configuration.nix b/hosts/lio/configuration.nix index 5c1645be..c3256ff6 100644 --- a/hosts/lio/configuration.nix +++ b/hosts/lio/configuration.nix @@ -55,6 +55,11 @@ nodejs_24 foot vlc - google-chrome + (google-chrome.override { + commandLineArgs = [ + "--remote-debugging-port=9222" + "--remote-allow-origins=*" + ]; + }) ]; } diff --git a/hosts/lio/flake.lock b/hosts/lio/flake.lock index 9ce8c34e..f5023026 100644 --- a/hosts/lio/flake.lock +++ b/hosts/lio/flake.lock @@ -63,20 +63,14 @@ }, "common": { "locked": { - "dir": "flakes/common", - "lastModified": 1769438846, - "narHash": "sha256-ahQYSazuB2RpF3XUYqKdwgOBFSbGUB2zQsqKEkSOuxA=", - "ref": "refs/heads/master", - "rev": "4bb36c0f7570b271bbeda67f9c4d5160c819850a", - "revCount": 1176, - "type": "git", - "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" + "path": "../../flakes/common", + "type": "path" }, "original": { - "dir": "flakes/common", - "type": "git", - "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" - } + "path": "../../flakes/common", + "type": "path" + }, + "parent": [] }, "crane": { "locked": { @@ -1341,11 +1335,11 @@ "nixpkgs": "nixpkgs_5" }, "locked": { - "lastModified": 1769445245, - "narHash": "sha256-ZQ+zGDomj4LmJLKqhF7KciMEAZyRDYouotl/u6KOyrE=", + "lastModified": 1769538000, + "narHash": "sha256-viIWuCjUqDk/QuIhUK9nOvDHCDHEO+VQRTnI/xhi/Ok=", "ref": "refs/heads/main", - "rev": "3e772152ad1ee211b88b4efebeb6191f55e0d91c", - "revCount": 9, + "rev": "221d0ca596b994f1942a42ee53a8b4508e23e7af", + "revCount": 20, "type": "git", "url": "https://git.joshuabell.xyz/ringofstorms/qvm" }, diff --git a/hosts/lio/flake.nix b/hosts/lio/flake.nix index 737f9394..9efec566 100644 --- a/hosts/lio/flake.nix +++ b/hosts/lio/flake.nix @@ -6,8 +6,8 @@ nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; # Use relative to get current version for testing - # common.url = "path:../../flakes/common"; - common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common"; + common.url = "path:../../flakes/common"; + # common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common"; # secrets.url = "path:../../flakes/secrets"; secrets.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/secrets"; # secrets-bao.url = "path:../../flakes/secrets-bao";