Skip to content

Instantly share code, notes, and snippets.

@brackendev
Last active October 15, 2025 13:46
Show Gist options
  • Select an option

  • Save brackendev/705e754e599e451343226e9a37ccf4c6 to your computer and use it in GitHub Desktop.

Select an option

Save brackendev/705e754e599e451343226e9a37ccf4c6 to your computer and use it in GitHub Desktop.
Git LLM - LLM-powered commit messages and PR descriptions
#!/usr/bin/env bb
;; ============================================================================
;; Git LLM - LLM-powered commit messages and PR descriptions
;; ============================================================================
;; This is a Babashka script - a fast-starting scripting environment for Clojure
;; Learn more at https://babashka.org/
;;
;; This script uses LLMs (Claude or Codex) to generate git commit messages and
;; PR descriptions based on code changes. It can either preview the generated
;; text (--dry-run) or automatically commit/push or create the PR.
;;
;; Source: https://gist.github.com/brackendev/705e754e599e451343226e9a37ccf4c6
;;
;; Dependencies:
;; brew install gum # Required for interactive mode (-i flag)
;; brew install gh # Required for PR mode (--pr flag)
;;
;; Usage: git_llm.bb [options]
;; -m opus|sonnet|gpt-5-codex Use Claude Opus, Sonnet, or GPT-5-Codex (default: gpt-5-codex)
;; -v Show verbose output
;; -i Interactive mode - edit messages and confirm actions
;; --commit Generate commit message, commit staged changes, and push
;; --pr Generate PR description and create PR
;; --dry-run Show generated text without committing/creating PR
;; -h Show help
;;
;; Examples:
;; git_llm.bb --commit --dry-run # Preview commit message
;; git_llm.bb --commit -i # Edit message before committing
;; git_llm.bb --pr --dry-run # Preview PR description
;; git_llm.bb --pr -i # Edit description before creating PR
;; Load dependencies at runtime
(require '[babashka.deps :as deps])
(deps/add-deps '{:deps {progrock/progrock {:mvn/version "1.0.0"}
clojure-term-colors/clojure-term-colors {:mvn/version "0.1.0"}}})
;; Import required libraries:
;; - babashka.process for running shell commands
;; - babashka.cli for parsing command-line arguments
;; - clojure.string for string manipulation
;; - progrock.core for progress indicators
;; - clojure.term.colors for colored terminal output
(require '[babashka.process :refer [shell]]
'[babashka.cli :as cli]
'[clojure.string :as str]
'[progrock.core :as pr]
'[clojure.term.colors :as c])
;; Configuration - Edit these values to customize the script
;; def creates an immutable global variable
(def config
{:default-model "gpt-5-codex" ; Default LLM model (sonnet, opus, or gpt-5-codex)
:max-diff-chars 8000 ; Max characters from diff to send to LLM
:valid-models #{"sonnet" "opus" "gpt-5-codex"} ; #{} creates a set - fast membership testing
;; Map models to their CLI providers
;; This determines which command-line tool to use for each model
:model-providers {"gpt-5-codex" "codex" ; Codex models use the codex CLI
"sonnet" "claude" ; Claude Sonnet uses claude CLI
"opus" "claude"} ; Claude Opus uses claude CLI
;; Commit message generation prompt
:commit-prompt-template
(str "Based on this git diff of staged changes, "
"write a single line commit message using imperative mood "
"(e.g., 'Add feature' not 'Added feature' or 'I added feature'). "
"No quotes, no explanation, just the message:")
;; PR template file path - project-specific customization
;; If the file doesn't exist, falls back to a default template
:pr-template-file "docs/PULL_REQUEST_TEMPLATE.md"})
;; Define the command-line options the script accepts
;; babashka.cli uses this to parse arguments like -m, --model, -v, etc.
(def options
{:model {:default (:default-model config) :alias :m} ; :m is short for --model
:verbose {:alias :v} ; :v is short for --verbose
:commit {} ; --commit for generating and committing
:pr {} ; --pr for PR creation
:dry-run {} ; --dry-run to preview without executing
:interactive {:alias :i} ; :i is short for --interactive
:help {:alias :h}}) ; :h is short for --help
(defn print-help
"Print usage instructions and exit"
[]
(println "Generate commit messages, commit and push changes, and create PRs using Claude or Codex\n")
(println "Usage: git_llm.bb [options]\n")
(println " -m MODEL Use opus, sonnet, or gpt-5-codex (default: gpt-5-codex)")
(println " -v Show verbose output")
(println " -i Interactive mode - edit messages and confirm actions")
(println " --commit Generate commit message, commit staged changes, and push")
(println " --pr Generate PR description and create PR")
(println " --dry-run Show generated text without committing/creating PR")
(println " -h Show this help")
(println "\nExamples:")
(println " git_llm.bb --commit --dry-run # Show commit message")
(println " git_llm.bb --commit -i # Edit message before committing")
(println " git_llm.bb --pr --dry-run # Show PR description")
(println " git_llm.bb --pr -i # Edit description before creating PR"))
(defn run-git
"Run a git command and return its output as a string
Example: (run-git \"diff\" \"--cached\") returns the staged diff
The & args means this function accepts any number of arguments"
[& args]
;; -> is the 'thread-first' macro - passes result of each form as first arg to next
;; This is equivalent to: (:out (apply shell {:out :string :continue true} "git" args))
(-> (apply shell {:out :string :continue true} "git" args)
:out))
(defn with-spinner
"Execute a task while showing a spinner with a status message
Parameters:
- message: The text to display next to the spinner
- f: A function to execute while the spinner runs"
[message f]
;; let creates local variables - these exist only within this function
(let [done (atom false) ; atom = mutable reference, starts as false
spinner-chars ["⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏"] ; Braille spinner animation frames
;; future runs code in a background thread (like setTimeout in JavaScript)
;; This allows the spinner to animate while the main task runs
spinner-thread (future
;; loop/recur creates a loop (like while in other languages)
;; recur jumps back to loop with new values
(loop [i 0]
;; when-not = if not (checks if done is still false)
;; @ reads the current value of the atom
(when-not @done
;; Print spinner to stderr so it doesn't mix with normal output
(binding [*out* *err*]
;; \r = carriage return (moves cursor to start of line, overwrites previous spinner)
;; nth gets item at index i, mod wraps around when i > array length
;; c/yellow wraps the spinner char in yellow ANSI color codes
(print (str "\r" (c/yellow (nth spinner-chars (mod i (count spinner-chars)))) " " message))
(flush)) ; flush ensures print shows immediately
(Thread/sleep 100) ; Wait 100ms between frames
(recur (inc i)))))] ; Loop with i+1 (next frame)
;; While spinner runs in background, execute the actual task
(let [result (f)]
;; Task is done - stop the spinner
(reset! done true) ; reset! changes atom value to true
@spinner-thread ; Wait for spinner thread to finish (@ = deref)
;; Clear the spinner line by overwriting with spaces
(binding [*out* *err*]
(print "\r") ; Move to start of line
;; Create string of spaces as long as the spinner message
;; repeat creates N copies, apply str joins them into one string
(print (apply str (repeat (+ 2 (count message)) " ")))
(print "\r") ; Move cursor back to start
(flush))
result)))
(defn gum-write
"Open gum editor for multi-line text editing
Parameters:
- value: Initial text to show in the editor
- header: Header text shown above the editor
Returns the edited text as a string"
[value header]
;; -> is the thread-first macro - pipes result through functions
;; This reads as: shell() -> get :out -> trim whitespace
(-> (shell {:out :string :in value} ; :out :string = capture output, :in value = pipe value to stdin
"gum" "write" "--header" header "--char-limit" "0")
:out ; Extract :out key from result map
str/trim)) ; Remove leading/trailing whitespace
(defn gum-confirm
"Show a yes/no confirmation prompt
Parameters:
- prompt: The question to ask the user
Returns true if user confirms (yes), false otherwise (no)"
[prompt]
;; gum confirm exits with 0 for yes, 1 for no
;; zero? checks if exit code is 0 (success)
;; So: exit 0 -> zero? returns true, exit 1 -> zero? returns false
(zero? (:exit (shell {:continue true} ; :continue true = don't throw error on non-zero exit
"gum" "confirm" prompt))))
(defn call-llm
"Call the appropriate LLM CLI based on model and return the response
Parameters:
- model: Which LLM model to use (sonnet, opus, or gpt-5-codex)
- prompt: The prompt text to send to the LLM
Returns the LLM response as a string"
[model prompt]
;; Look up which CLI provider to use for this model
;; Example: "gpt-5-codex" -> "codex", "sonnet" -> "claude"
(let [provider ((:model-providers config) model)]
(if (= provider "codex")
;; Codex CLI: uses tmpfile to capture output and redirects noise to /dev/null
;; Codex is verbose and writes to /dev/tty, so this approach silences it
(let [tmpfile (str "/tmp/git_llm_" (System/currentTimeMillis) ".txt")]
(shell {:out :string :err :string :continue true}
"sh" "-c"
;; Build command: codex --model MODEL exec --output-last-message TMPFILE "PROMPT"
;; Redirect all output to /dev/null to suppress verbose progress messages
(str "codex --model " model " exec --output-last-message " tmpfile " \"$1\" </dev/null >/dev/null 2>&1")
"sh" prompt)
;; Read result from tmpfile, clean up, and return
(let [result (slurp tmpfile)]
(shell "rm" tmpfile)
(str/trim result)))
;; Claude CLI: simpler because claude is quiet by default
;; Just run claude and capture stdout directly
(-> (shell {:out :string :err :string :continue true}
"claude" "--model" model "-p" prompt)
:out
str/trim))))
(defn get-commit-message
"Ask Claude or Codex to generate a commit message based on the staged changes
Parameters:
- files: List of changed filenames (from git diff --cached --name-only)
- diff: The actual code changes (from git diff --cached)
- model: Which LLM to use (sonnet, opus, or gpt-5-codex)
- verbose?: Boolean flag for extra debug output
Returns the generated commit message as a string"
[files diff model verbose?]
;; let creates local variables that only exist within this function's scope
(let [prompt (str (:commit-prompt-template config) "\n\n" ; :keyword gets value from map
"Files changed:\n" files "\n\n"
"Diff:\n"
;; Truncate diff to max length to avoid exceeding LLM token limits
;; subs = substring, min returns smaller of two numbers
;; Example: if diff is 10000 chars and max is 8000, takes first 8000 chars
(subs diff 0 (min (:max-diff-chars config) (count diff))))]
;; when is like 'if' but only for when condition is true (no else branch)
(when verbose?
;; binding temporarily changes where println outputs go
;; *out* is stdout, *err* is stderr - redirects to stderr for debug output
(binding [*out* *err*]
(println "Model:" model)
(println "Prompt length:" (count prompt) "chars")))
;; Call LLM with spinner animation
;; with-spinner shows animated spinner while LLM generates response
(with-spinner "Generating commit message..."
;; fn [] creates an anonymous function with no arguments
;; This is passed to with-spinner which will call it
;; call-llm handles provider-specific logic (codex vs claude CLI)
(fn []
(call-llm model prompt)))))
(defn get-pr-description
"Generate a PR description based on commits in the current branch
Parameters:
- model: Which LLM to use (sonnet, opus, or gpt-5-codex)
- verbose?: Boolean flag for extra debug output
Compares current branch against the default branch and generates a full PR template
Returns the generated PR description as a string"
[model verbose?]
;; Get the current branch name and determine the base branch to compare against
(let [current-branch (str/trim (run-git "branch" "--show-current"))
;; Try to auto-detect the default branch from origin/HEAD, fallback to origin/main
;; or returns first truthy value: if first fails, uses second
base-branch (str/trim (or (run-git "symbolic-ref" "refs/remotes/origin/HEAD")
"origin/main"))
;; Strip the "refs/remotes/origin/" prefix to get just the branch name
;; #"..." creates a regex pattern (# prefix means regex literal)
;; Example: "refs/remotes/origin/main" becomes "main"
base-branch (str/replace base-branch #"refs/remotes/origin/" "")
;; Get all commits between base and current branch
;; .. syntax shows commits in current that aren't in base
;; Example: if base is main and current is feature-branch:
;; shows all commits added to feature-branch since branching from main
commits (run-git "log" (str base-branch ".." current-branch) "--oneline")
;; Get the full diff (using ... for merge-base comparison)
;; ... syntax compares against common ancestor (merge-base), not tip of base
;; This shows only changes made on the current branch
;; Example: if main has moved forward, ... ignores those new main commits
diff (run-git "diff" (str base-branch "..." current-branch))
;; Try to load PR template from file, fall back to default if not found
;; try/catch handles cases where template file doesn't exist or can't be read
;; Catches any Exception (file not found, permission denied, etc.) and uses minimal default
pr-template (try
(slurp (:pr-template-file config))
(catch Exception _
(str "## Description\n\n"
"## Related Issue\n\n"
"## Motivation and Context\n\n"
"## How Has This Been Tested?\n\n"
"## Screenshots (if appropriate):\n\n")))
;; Build the prompt with commits, diff, and the PR template to fill in
prompt (str "Based on these git commits and diff, generate a PR description.\n\n"
"Commits:\n" commits "\n\n"
"Diff:\n" (subs diff 0 (min (:max-diff-chars config) (count diff))) "\n\n"
"Fill in this template with appropriate content:\n\n"
pr-template
"\n\nReturn the filled-in template with actual content (not the HTML comments)."
"\nDo not include any # heading above the first ## section."
"\nStart directly with ## Description.")]
(when verbose?
(binding [*out* *err*]
(println "Current branch:" current-branch)
(println "Base branch:" base-branch)
(println "Commits:\n" commits)))
;; Generate PR description using the selected model
;; call-llm handles provider-specific logic (codex vs claude CLI)
(with-spinner "Generating PR description..."
(fn []
(call-llm model prompt)))))
(defn get-pr-labels
"Select appropriate labels for the PR based on the changes
Parameters:
- commits: List of commit messages (from git log)
- diff: The actual code changes (from git diff)
- model: Which LLM to use (sonnet, opus, or gpt-5-codex)
- verbose?: Boolean flag for extra debug output
Flow:
1. Fetch all labels from repo with 'gh label list'
2. Send labels + changes to LLM asking it to select appropriate ones
3. Parse comma-separated response into vector of label names
Returns vector of label names, or empty vector if no labels selected"
[commits diff model verbose?]
;; Get available labels from the repo using gh CLI
(let [available-labels (-> (shell {:out :string :continue true} "gh" "label" "list" "--limit" "100")
:out ; Extract stdout from shell result
str/split-lines ; Split into array of lines
;; Each line is "name\tdescription\tcolor" - extract just the name
;; ->> is thread-last macro: passes result as LAST argument
;; #(...) is shorthand for anonymous function: #(+ % 1) = (fn [x] (+ x 1))
;; % is the argument (in this case, each line)
;; #"\t" is a regex that matches tab character
;; So this: splits each line on tabs, takes first element (the label name)
(->> (map #(first (str/split % #"\t")))))
;; Build prompt asking LLM to select labels
prompt (str "Based on these git commits and diff, select appropriate labels from the available labels.\n\n"
"Commits:\n" commits "\n\n"
"Diff:\n" (subs diff 0 (min (:max-diff-chars config) (count diff))) "\n\n"
"Available labels:\n" (str/join ", " available-labels) "\n\n"
"Return ONLY the selected label names as a comma-separated list (e.g., bug, enhancement).\n"
"If no labels apply, return 'none'.\n"
"Do not include explanations or additional text.")]
(when verbose?
(binding [*out* *err*]
(println "Available labels:" (str/join ", " available-labels))))
;; Call LLM to select labels
;; call-llm handles provider-specific logic (codex vs claude CLI)
(let [response (with-spinner "Selecting labels..."
(fn []
(call-llm model prompt)))
;; Parse response into vector of labels
;; If response is empty or "none", return empty vector
;; Otherwise split on commas, trim whitespace, and convert to vector
labels (if (or (empty? response) (= "none" (str/lower-case response)))
[]
(vec (map str/trim (str/split response #","))))]
(when verbose?
(binding [*out* *err*]
(println "LLM response:" response)
(println "Parsed labels:" labels)))
labels)))
;; ============================================================================
;; Main execution starts here
;; ============================================================================
;; The script has two main modes: --commit and --pr
;; Both support --dry-run to preview without taking action
;; Parse command-line arguments and validate git repo state before executing
;; let creates local variables for the entire main execution block
(let [;; Parse command-line arguments into a map
;; {:keys [a b]} is destructuring - extracts specific keys from a map
;; Example: {:model "sonnet" :verbose true} -> model = "sonnet", verbose = true
;; *command-line-args* is a special Babashka variable containing script arguments
{:keys [model verbose help pr dry-run commit interactive]} (cli/parse-opts *command-line-args*
{:spec options})
;; delay creates a "lazy" value - the code inside won't run until @ is used
;; This is efficient: if help is requested, git checks are skipped entirely
;; @ (deref) is used later to actually run the delayed code and get the result
;; These checks are delayed because they're expensive and might not be needed
git-check (delay (shell {:out :string :err :string :continue true}
"git" "rev-parse" "--git-dir")) ; Check if in git repo
staged-files (delay (run-git "diff" "--cached" "--name-only"))] ; Get list of staged files
;; cond is like a switch/case statement - evaluates conditions in order
;; First truthy condition wins, its expression is evaluated, rest are skipped
;; This structure validates arguments and environment before running any commands
(cond
;; Show help if no args or help flag
;; or returns true if ANY condition is true
(or help (empty? *command-line-args*))
(print-help)
;; Invalid model specified - check if model is in the valid set
;; Sets are functions! (#{:a :b} :a) returns :a (truthy), (#{:a :b} :c) returns nil (falsy)
;; So (:valid-models config) acts as a function that checks membership
;; not inverts: if model not in set, this condition is true
(not ((:valid-models config) model))
;; do groups multiple expressions (executes all, returns last)
(do (binding [*out* *err*] ; Print to stderr
(println (c/red (str "Error: Model must be one of: "
(str/join ", " (:valid-models config))))))
(System/exit 1)) ; Exit with error code
;; Not in a git repository - validate environment
;; @ dereferences the delay - NOW the git check actually runs
;; :exit extracts the exit code from the shell result map
;; zero? checks if exit code is 0 (success), not inverts it
;; So: if exit code is not 0 (command failed), this condition is true
(not (zero? (:exit @git-check)))
(do (binding [*out* *err*]
(println (c/red "Error: Not in a git repository")))
(println "This script must be run from within a git repository.")
(System/exit 1))
;; Interactive mode requires gum - validate dependencies
;; and checks both conditions: interactive flag AND gum not installed
;; command -v returns 0 if command exists, non-zero if not
(and interactive
(not (zero? (:exit (shell {:out :string :err :string :continue true}
"command" "-v" "gum")))))
(do (binding [*out* *err*]
(println (c/red "Error: Interactive mode requires gum")))
(println (c/cyan "Install with: brew install gum"))
(System/exit 1))
;; PR mode requires gh (GitHub CLI)
;; Check if gh is installed when --pr flag is used
(and pr
(not (zero? (:exit (shell {:out :string :err :string :continue true}
"command" "-v" "gh")))))
(do (binding [*out* *err*]
(println (c/red "Error: PR mode requires gh (GitHub CLI)")))
(println (c/cyan "Install with: brew install gh"))
(System/exit 1))
;; PR mode: preview description (--pr --dry-run)
;; Generate and display PR description and selected labels without creating the PR
;; In interactive mode, allows editing before displaying
;; Note: Duplicates git logic here and in PR create mode because get-pr-description
;; does its own git calls internally, but get-pr-labels needs the commits/diff
(and pr dry-run)
(let [current-branch (str/trim (run-git "branch" "--show-current"))
base-branch (str/trim (or (run-git "symbolic-ref" "refs/remotes/origin/HEAD")
"origin/main"))
base-branch (str/replace base-branch #"refs/remotes/origin/" "")
;; Get commits (.. = all commits in current not in base)
commits (run-git "log" (str base-branch ".." current-branch) "--oneline")
;; Get diff (... = compare against merge-base, shows only branch changes)
diff (run-git "diff" (str base-branch "..." current-branch))
generated-description (get-pr-description model verbose)
;; In interactive mode, allows editing before displaying
description (if interactive
(gum-write generated-description "Edit PR description:")
generated-description)
selected-labels (get-pr-labels commits diff model verbose)]
(println description)
(println "\n---")
(println "Labels:" (str/join ", " selected-labels)))
;; PR mode: create PR (--pr)
;; Generate PR description and create the PR using gh CLI
;; --fill automatically uses branch name as PR title
;; Labels are selected by LLM based on the changes
pr
(let [current-branch (str/trim (run-git "branch" "--show-current"))
base-branch (str/trim (or (run-git "symbolic-ref" "refs/remotes/origin/HEAD")
"origin/main"))
base-branch (str/replace base-branch #"refs/remotes/origin/" "")
;; Get commits (.. = all commits in current not in base)
commits (run-git "log" (str base-branch ".." current-branch) "--oneline")
;; Get diff (... = compare against merge-base, shows only branch changes)
diff (run-git "diff" (str base-branch "..." current-branch))
generated-description (get-pr-description model verbose)
;; In interactive mode, allows editing the description
description (if interactive
(gum-write generated-description "Edit PR description:")
generated-description)
;; Ask LLM to select appropriate labels based on changes
selected-labels (get-pr-labels commits diff model verbose)
;; Build label arguments for gh CLI dynamically
;; gh pr create expects: -l label1 -l label2 -l label3
;; mapcat = map + concat: transforms each item and flattens results
;; Example: ["bug" "enhancement"] -> [["-l" "bug"] ["-l" "enhancement"]] -> ["-l" "bug" "-l" "enhancement"]
;; fn [label] ["-l" label] creates a function that wraps each label with "-l"
label-args (mapcat (fn [label] ["-l" label]) selected-labels)
;; Combine base command with dynamic label arguments
;; concat joins multiple sequences into one
;; Example result: ["gh" "pr" "create" "--body" "description" "--fill" "-l" "bug" "-l" "enhancement"]
pr-args (concat ["gh" "pr" "create" "--body" description "--fill"] label-args)]
;; In interactive mode, confirm before creating PR
;; when executes body only if condition is true
;; (or (not interactive) ...) = if not interactive OR user confirms
(when (or (not interactive)
(gum-confirm "Create PR?"))
;; Execute gh command with all arguments
;; apply calls a function with arguments from a sequence
;; cons adds {:continue true} as first argument to the shell function
;; This expands to: (shell {:continue true} "gh" "pr" "create" "--body" "..." "--fill" "-l" "bug" ...)
(apply shell (cons {:continue true} pr-args))
(when verbose
(binding [*out* *err*]
(println "PR Description:\n" description)
(println "Labels:" selected-labels)))))
;; Commit mode: check for staged files first
;; Can't commit if nothing is staged - fail early with clear error
;; and checks both: commit mode AND no staged files
;; @staged-files derefs the delay - NOW the git diff command actually runs
;; empty? returns true if the string is empty (no files listed)
(and commit (empty? @staged-files))
(do (binding [*out* *err*]
(println (c/red "Error: No staged files")))
(println (c/cyan "Use 'git add' to stage changes first."))
(System/exit 1))
;; Commit mode: preview commit message (--commit --dry-run)
;; Generate and display commit message without committing
;; In interactive mode, allows editing before displaying
(and commit dry-run)
(let [diff (run-git "diff" "--cached") ; Get staged changes
;; @staged-files derefs the delay to get the file list
generated-message (get-commit-message @staged-files diff model verbose)
;; In interactive mode, allows editing before displaying
message (if interactive
(gum-write generated-message "Edit commit message:")
generated-message)]
(println message)) ; Output the message (dry-run = preview only)
;; Commit mode: commit and push (--commit)
;; Generate commit message, commit staged changes, and push to remote
;; This is the actual commit flow (not dry-run)
commit
(let [diff (run-git "diff" "--cached")
generated-message (get-commit-message @staged-files diff model verbose)
;; In interactive mode, allows editing the message
message (if interactive
(gum-write generated-message "Edit commit message:")
generated-message)]
(when verbose
(binding [*out* *err*]
(println "Commit message:" message)))
;; In interactive mode, confirm before committing
;; when = execute body only if condition is true
(when (or (not interactive) ; Not interactive OR user confirmed
(gum-confirm "Commit and push?"))
(shell "git" "commit" "-m" message) ; Create the commit
;; Push to remote with upstream tracking
;; -u sets upstream tracking (needed for new branches)
;; origin is the remote name (typically the GitHub repo)
;; HEAD resolves to current branch name automatically (works for any branch)
(shell "git" "push" "-u" "origin" "HEAD")))
;; No mode specified - catch-all for invalid usage
;; :else is the default case in cond (like 'default:' in switch statements)
;; If none of the above conditions matched, show error
:else
(do (binding [*out* *err*]
(println (c/red "Error: Must specify --commit or --pr")))
(println (c/cyan "Use -h for help"))
(System/exit 1))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment