├── README.md ├── autocommitmesssage └── autocommitmessage.sh ├── autogit └── autogit.sh ├── autopullrequest └── autopullrequest.sh ├── autoreview └── autoreview.sh └── docs ├── autocommitmessage.gif ├── autogit.gif ├── automations-logo.png ├── automations.gif ├── autopullrequest-example.png ├── autopullrequest.gif ├── autoreview-example.png ├── autoreview.gif └── usage.md /README.md: -------------------------------------------------------------------------------- 1 | # Automations 2 | 3 | Automations are shell scripts I wrote alongside generative AI, that leverage generative AI to make common developer tasks delightful and efficient. 4 | 5 | ![Shell automations for productivity and fun](./docs/automations.gif) 6 | 7 | ## Current automations 8 | * [`autogit`](#autogit) 9 | * [`autoreview`](#autoreview) 10 | * [`autocommitmessage`](#autocommitmessage) 11 | * [`autopullrequest`](#autopullrequest) 12 | 13 | ## Installation & usage 14 | 15 | [Usage guide](./docs/usage.md) 16 | 17 | ## `autogit` 18 | 19 | autogit's goal is to ensure you're always working with the latest code, because even experienced developers forget to `git pull` at the least opportune times. 20 | 21 | ![autogit is a shell script that handles git fetching, branch pruning and more](./docs/autogit.gif) 22 | 23 | [**Read the deep-dive blog post on `autogit`** to learn about everything it can do.](https://www.zackproser.com/blog/autogit-introduction) 24 | 25 | ## `autoreview` 26 | 27 | `autoreview` is a shell script that performs a detailed code review of your stashed git changes. 28 | 29 | ![Shell automation for automatic local code review](./docs/autoreview.gif) 30 | 31 | Here's an example of a code review you'd get back, right in your terminal, so that you can refer to the review while you fix your code in another buffer: 32 | 33 | ![Example autoreview review output](./docs/autoreview-example.png) 34 | 35 | ## `autocommitmessage` 36 | 37 | Never write "check in latest" ever again. Always get accurate, well-formed git messages that actually record the context of the changes you're making for posterity 38 | 39 | ![auto git commit message writer](./docs/autocommitmessage.gif) 40 | 41 | ## `autopullrequest` 42 | 43 | [Example pull request opened by this command.](https://github.com/zackproser/sizeof/pull/2) 44 | 45 | Reads all the commit messages for commits that exist in your current branch but not in your default branch. Writes a pull request description and title for you, then uses the `gh` tool to programmatically open the pull request for you. Pairs very nicely with `autocommitmessage`. 46 | 47 | ![autopullrequest](./docs/autopullrequest.gif) 48 | 49 | Here's an example of a pull request opened by this automation - in fact, I ran it on this codebase itself to open my most recent pr! 50 | 51 | ![autopullrequest example](./docs/autopullrequest-example.png) 52 | -------------------------------------------------------------------------------- /autocommitmesssage/autocommitmessage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | check_dependencies() { 4 | local DEPS=("mods" "gum") 5 | local MISSING_DEPS=() 6 | 7 | for dep in "${DEPS[@]}"; do 8 | if ! command -v $dep &> /dev/null; then 9 | MISSING_DEPS+=($dep) 10 | fi 11 | done 12 | 13 | if [ ${#MISSING_DEPS[@]} -ne 0 ]; then 14 | echo "The following dependencies are missing:" 15 | for dep in "${MISSING_DEPS[@]}"; do 16 | case $dep in 17 | "mods") 18 | echo "- mods: https://github.com/charmbracelet/mods" 19 | ;; 20 | "gum") 21 | echo "- gum: https://github.com/charmbracelet/gum" 22 | ;; 23 | esac 24 | done 25 | exit 1 26 | fi 27 | } 28 | 29 | autocommitmessage() { 30 | msg="$(git diff --cached | mods --status-text "Writing commit message" "write a commit message for this diff")" && \ 31 | printf '\n' && \ 32 | gum write --header=" Look good? Ctrl+D to commit." --value="$msg" && \ 33 | git commit -am "$msg" 34 | } 35 | 36 | autocommitmessage 37 | -------------------------------------------------------------------------------- /autogit/autogit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Initialize the previous directory 4 | prev_dir="" 5 | 6 | # Function to check dependencies 7 | check_dependencies() { 8 | local DEPS=("mods") 9 | local MISSING_DEPS=() 10 | 11 | for dep in "${DEPS[@]}"; do 12 | if ! command -v $dep &> /dev/null; then 13 | MISSING_DEPS+=($dep) 14 | fi 15 | done 16 | 17 | if [ ${#MISSING_DEPS[@]} -ne 0 ]; then 18 | echo "The following dependencies are missing:" 19 | for dep in "${MISSING_DEPS[@]}"; do 20 | case $dep in 21 | "mods") 22 | echo "- mods: https://github.com/charmbracelet/mods" 23 | ;; 24 | esac 25 | done 26 | exit 1 27 | fi 28 | } 29 | 30 | # Function to determine if the current working directory is a git repository 31 | function is_git_repository() { 32 | git -C . rev-parse 2> /dev/null 33 | } 34 | 35 | # Function to check for uncommitted changes 36 | function has_uncommitted_changes() { 37 | git diff-index --quiet HEAD -- 38 | if [ $? -ne 0 ]; then 39 | return 0 # has uncommitted changes 40 | fi 41 | return 1 # no uncommitted changes 42 | } 43 | 44 | # Prompt the user before stashing changes 45 | function prompt_stash_changes() { 46 | read -p "You have uncommitted changes. Do you want to stash them? [y/N] " answer 47 | if [[ "$answer" =~ ^[Yy]$ ]]; then 48 | return 1 # stash changes 49 | fi 50 | return 0 # do not stash changes 51 | } 52 | 53 | # Determine the root directory of the current git repository 54 | function get_repo_root() { 55 | git rev-parse --show-toplevel 2> /dev/null 56 | } 57 | 58 | # Function to check if the user has changed to a subdirectory of the repo 59 | function changed_to_subdir() { 60 | local repo_root 61 | repo_root=$(get_repo_root) 62 | if [[ -z "$repo_root" ]]; then 63 | # not a git repository 64 | return 1 65 | fi 66 | local cwd 67 | cwd=$(realpath .) 68 | [[ "$cwd"/ != "$repo_root"/ && "$cwd"/ != "$repo_root/"* ]] 69 | } 70 | 71 | # Handle the case where the local repository's default branch has changed 72 | function check_branch_switch() { 73 | local default_branch 74 | default_branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') 75 | 76 | git fetch origin "$default_branch" 77 | git checkout "$default_branch" && git pull origin "$default_branch" 78 | 79 | } 80 | 81 | # Check if the repository needs to be updated 82 | function needs_pull() { 83 | git fetch --all --prune 84 | ! git diff --quiet HEAD "origin/$(git symbolic-ref --short HEAD)" 2> /dev/null && return 0 # needs to be updated 85 | return 1 # up to date 86 | } 87 | 88 | autogit() { 89 | if is_git_repository; then 90 | 91 | # If the user navigates to a sub-directory of the original git repo, then we should not run autogit again 92 | if changed_to_subdir; then 93 | return 94 | fi 95 | 96 | # Check if the repository has any commits 97 | if [ -z "$(git rev-parse HEAD 2>/dev/null)" ]; then 98 | echo "The repository is brand new and doesn't have any commits." 99 | return 100 | fi 101 | 102 | # Check if the repository has a remote set up 103 | if [ -z "$(git config --get remote.origin.url)" ]; then 104 | echo "The repository doesn't have a remote set up." 105 | return 106 | fi 107 | 108 | # Check if the repository needs to be updated 109 | if ! needs_pull; then 110 | echo "The repository is already up to date." 111 | return 112 | fi 113 | 114 | # Check for uncommitted changes 115 | if has_uncommitted_changes; then 116 | if prompt_stash_changes; then 117 | # User chose not to stash changes, abort 118 | echo "Aborting: uncommitted changes detected." 119 | return 120 | fi 121 | # Store the current branch as dirty (with stashed changes) 122 | dirty_branch=$(git branch --show-current) 123 | git stash save -u "autogit_$(date +%s)" > /dev/null 2>&1 124 | if git stash list | grep -q "autogit_"; then 125 | echo "Local changes stashed." 126 | fi 127 | fi 128 | 129 | # Save the current branch 130 | current_branch=$(git branch --show-current) 131 | 132 | # Check if the local repository's default branch has changed 133 | check_branch_switch 134 | 135 | # Pull changes to the default branch 136 | git checkout "$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')" 137 | git pull 138 | 139 | # Reset to the saved branch 140 | git checkout "$current_branch" 141 | 142 | # Unstash changes if necessary 143 | if [[ -n "$dirty_branch" ]]; then 144 | if git stash list | grep -q "autogit_"; then 145 | git stash apply "$(git stash list | grep "autogit_" | awk '{print $1}')" 146 | echo "Stashed changes applied to $dirty_branch." 147 | fi 148 | # Reset dirty_branch variable 149 | dirty_branch="" 150 | fi 151 | 152 | # Notify the user of successful completion 153 | echo "Autogit has successfully updated the repository." 154 | fi 155 | } 156 | 157 | run_autogit() { 158 | # Ensure user has dependencies installed 159 | check_dependencies 160 | 161 | local current_dir 162 | current_dir=$(pwd) 163 | for subdir in $(find "$current_dir" -type d -name .git); do 164 | cd "$(dirname "$subdir")" # change to the repository directory 165 | if [ "$prev_dir" != "$(pwd)" ]; then 166 | prev_dir="$(pwd)" 167 | autogit 168 | fi 169 | done 170 | } 171 | 172 | run_autogit 173 | -------------------------------------------------------------------------------- /autopullrequest/autopullrequest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | check_dependencies() { 4 | local DEPS=("mods" "gum" "glow" "gh") 5 | local MISSING_DEPS=() 6 | 7 | for dep in "${DEPS[@]}"; do 8 | if ! command -v $dep &> /dev/null; then 9 | MISSING_DEPS+=($dep) 10 | fi 11 | done 12 | 13 | if [ ${#MISSING_DEPS[@]} -ne 0 ]; then 14 | echo "The following dependencies are missing:" 15 | for dep in "${MISSING_DEPS[@]}"; do 16 | case $dep in 17 | "mods") 18 | echo "- mods: https://github.com/charmbracelet/mods" 19 | ;; 20 | "gum") 21 | echo "- gum: https://github.com/charmbracelet/gum" 22 | ;; 23 | "glow") 24 | echo "- glow: https://github.com/charmbracelet/glow" 25 | ;; 26 | "gh") 27 | echo "- gh: https://github.com/cli/cli" 28 | ;; 29 | esac 30 | done 31 | exit 1 32 | fi 33 | } 34 | 35 | # Function to determine if the current working directory is a git repository 36 | function is_git_repository() { 37 | git -C . rev-parse 2> /dev/null 38 | } 39 | 40 | function get_default_branch() { 41 | git remote show origin | awk '/HEAD branch/ {print $NF}' 42 | } 43 | 44 | function get_current_branch() { 45 | git rev-parse --abbrev-ref HEAD 46 | } 47 | 48 | function get_commits_for_branch() { 49 | git --no-pager log --pretty=format:"%s" ..."$(get_default_branch)" 50 | } 51 | 52 | function summarize_commit_messages() { 53 | readonly commit_messages="$1" 54 | 55 | local commit_summary 56 | commit_summary="$(echo "$commit_messages" | mods --status-text "Summarizing commits into PR description" "Summarize these git commits into a pull request description. Include a high level summary of what the changes do, context for the changes, and anything else commonly appearing in high quality pull request descriptions")" 57 | echo "$commit_summary" 58 | } 59 | 60 | function create_title_from_summary() { 61 | readonly commit_summary="$1" 62 | 63 | pr_title="$(echo "$commit_summary" | mods --status-text "Writing pull request title" "Write a pull request title based of this summary. Make sure it is concise yet perfectly descriptive of the changes")" 64 | echo "$pr_title" 65 | } 66 | 67 | function ensure_changes_pushed() { 68 | git push -u origin "$(get_current_branch)" 69 | } 70 | 71 | 72 | autopullrequest() { 73 | if ! is_git_repository; then 74 | echo "Not a git repository" 75 | exit 1 76 | fi 77 | 78 | check_dependencies 79 | 80 | commits=$(get_commits_for_branch) 81 | if [ -z "$commits" ]; then 82 | echo "No commits found" 83 | exit 1 84 | fi 85 | 86 | ensure_changes_pushed 87 | 88 | summary=$(summarize_commit_messages "$commits") 89 | 90 | title=$(create_title_from_summary "$summary") 91 | 92 | 93 | gh pr create --title "$title" --body "$summary" 94 | } 95 | 96 | autopullrequest 97 | 98 | -------------------------------------------------------------------------------- /autoreview/autoreview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check dependencies 4 | check_dependencies() { 5 | local DEPS=("mods" "glow") 6 | local MISSING_DEPS=() 7 | 8 | for dep in "${DEPS[@]}"; do 9 | if ! command -v $dep &> /dev/null; then 10 | MISSING_DEPS+=($dep) 11 | fi 12 | done 13 | 14 | if [ ${#MISSING_DEPS[@]} -ne 0 ]; then 15 | echo "The following dependencies are missing:" 16 | for dep in "${MISSING_DEPS[@]}"; do 17 | case $dep in 18 | "mods") 19 | echo "- mods: https://github.com/charmbracelet/mods" 20 | ;; 21 | "glow") 22 | echo "- glow: https://github.com/charmbracelet/glow" 23 | ;; 24 | esac 25 | done 26 | exit 1 27 | fi 28 | } 29 | 30 | # Function to get the repo name 31 | get_repo_name() { 32 | echo $(basename `git rev-parse --show-toplevel`) 33 | } 34 | 35 | # Function to generate a markdown file for the review 36 | generate_review_file() { 37 | local repo_name="$1" 38 | local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") 39 | echo "$AUTOREVIEW_DIR/$repo_name-$timestamp.md" 40 | } 41 | 42 | # Function to generate the review and write it to the review markdown file 43 | generate_review() { 44 | local review_filename="$1" 45 | printf "\n" >> "$review_filename" 46 | echo "# Code Review for $REPO_NAME at `date -u +"%Y-%dt%H:%M"`" >> "$review_filename" 47 | printf "\n" >> "$review_filename" 48 | echo "## Here's what I found" >> "$review_filename" 49 | git diff --cached | mods --status-text "Reviewing your code" "$REVIEW_PROMPT" >> "$review_filename" 50 | } 51 | 52 | # Check dependencies 53 | check_dependencies 54 | 55 | # Directory for review files, can be overridden by setting AUTOREVIEW_DIR environment variable 56 | AUTOREVIEW_DIR="${AUTOREVIEW_DIR:-$HOME/.autoreview}" 57 | 58 | # Review prompt, can be overridden by setting REVIEW_PROMPT environment variable 59 | REVIEW_PROMPT="${REVIEW_PROMPT:-* Please perform a code review on this source. List out any logical flaws or bugs you find, ranked in order of severity with the most severe issues presented first. When you spot a bug or issue, please always suggest a remediation. Include code snippets only when necessary to understand the issue.\n* Does the code follow common coding conventions and idioms for the language used? Does it include appropriate tests? If not, suggest initial tests that could be added.}" 60 | 61 | # Create the directory if it doesn't exist 62 | mkdir -p "$AUTOREVIEW_DIR" 63 | 64 | # Get repo name 65 | REPO_NAME=$(get_repo_name) 66 | 67 | # Create a filename with timestamp 68 | REVIEW_FILENAME=$(generate_review_file "$REPO_NAME") 69 | 70 | # Check if there are staged changes 71 | if git diff --cached --quiet; then 72 | echo "No staged changes to review" 73 | exit 0 74 | fi 75 | 76 | # Generate the review 77 | echo "Performing code review. Please sit tight..." 78 | generate_review "$REVIEW_FILENAME" 79 | 80 | # Output the review 81 | echo "\nReview file generated at: $REVIEW_FILENAME" 82 | glow "$REVIEW_FILENAME" 83 | 84 | -------------------------------------------------------------------------------- /docs/autocommitmessage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/autocommitmessage.gif -------------------------------------------------------------------------------- /docs/autogit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/autogit.gif -------------------------------------------------------------------------------- /docs/automations-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/automations-logo.png -------------------------------------------------------------------------------- /docs/automations.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/automations.gif -------------------------------------------------------------------------------- /docs/autopullrequest-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/autopullrequest-example.png -------------------------------------------------------------------------------- /docs/autopullrequest.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/autopullrequest.gif -------------------------------------------------------------------------------- /docs/autoreview-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/autoreview-example.png -------------------------------------------------------------------------------- /docs/autoreview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackproser/automations/3dfb352f8fa373096aa37cea4ffbb4e74174918f/docs/autoreview.gif -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | 2 | ## Installation & Usage 3 | 4 | ### Prerequisites 5 | 6 | Please ensure you have the following installed. The scripts will ensure they are installed and error out with a helpful message about where to find them if they are not. 7 | 8 | * [mods version v0.2.0 or later](https://github.com/charmbracelet/mods) 9 | * [glow](https://github.com/charmbracelet/glow) 10 | * [gum](https://github.com/charmbracelet/gum) 11 | 12 | ## Installation 13 | 14 | Generally speaking, `automations` are all shell scripts, so you can run them however you like. 15 | 16 | I tend to copy them from their source folders to `~/bin/` and then run `chmod +x ~/bin/` to make them executable. 17 | 18 | You could then choose to further `alias` them like so: 19 | 20 | ``` 21 | alias gcai="~/bin/autocommitmessage.sh" 22 | alias review="~/bin/autoreview.sh" 23 | alias autopr="~/bin/autopullrequest.sh" 24 | ``` 25 | The exact way you pull this off and where you write your aliases will differ slightly depending on your shell. I'll include shell-specific installation guides shortly. 26 | 27 | ## Script-specific integrations 28 | 29 | ### Integrating autogit/autogit.sh with your shell 30 | 31 | **Override your builtin cd command** 32 | 33 | This is currently saved to my `~/.zshrc` file. It works by overriding your built in change directory command, still calling 34 | 35 | This function assumes you have copied `autogit.sh` to `~/bin/autogit.sh`. If you have installed it elsewhere on your system, be sure to update the path in this function. 36 | 37 | Note this also assumes you have installed github.com/charmbracelet/gum. Be sure to run `zsh` in a new terminal or shell after updating your ~/.zshrc file for your changes to take effect. 38 | 39 | ``` 40 | function cd() { 41 | builtin cd "$@" && gum spin --title "Autogit updating git repo if necessary..." --show-output ~/bin/autogit.sh 42 | } 43 | ``` 44 | ### Integrating autocommitmessage with your shell 45 | 46 | **Create a new shell function named whatever you like** 47 | 48 | I picked `gcai`, because my git commit alias is already `gc` - so `gcai` stands for `git commit A.I.` 49 | 50 | ``` 51 | function gcai() { 52 | # This assumes you've already got an alias for the autocommitmessage script and have the script installed and made executable 53 | autocommitmessage 54 | } 55 | ``` 56 | --------------------------------------------------------------------------------