init commitly

This commit is contained in:
Adam Jeniski 2026-01-05 02:13:30 -05:00
commit 08e503f968
3 changed files with 169 additions and 0 deletions

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# commitly
A CLI tool for making commits to many subrepos after a distributed change.
## Overview
`commitly` is a Babashka/Clojure CLI tool designed for monorepo workflows where multiple independent git repositories coexist. When you make changes that span multiple subrepos (e.g., updating documentation, shared configuration, or architecture changes), `commitly` automates the process of committing those changes across all affected repositories.
## Features
- Automatically detects which subrepos have uncommitted changes
- Creates commits with a single message across all modified subrepos
- Reports commit status for each subrepo
- Written in Babashka for fast startup and easy distribution
## Usage
```bash
# Commit changes across all modified subrepos
./commitly "Your commit message here"
```
## Requirements
- [Babashka](https://babashka.org/) installed and available on PATH
## Installation
```bash
# Make the script executable
chmod +x commitly
# Optionally, symlink to a directory in your PATH
ln -s $(pwd)/commitly /usr/local/bin/commitly
```
## How It Works
1. Scans parent directory for git repositories
2. Checks each repository for uncommitted changes (staged or unstaged)
3. Commits changes in each modified repository with the provided message
4. Reports success/failure for each repository
## Use Case
Perfect for monorepos containing multiple independent services (each with their own `.git` directory) where cross-cutting changes need to be committed atomically with consistent commit messages.

7
bb.edn Normal file
View File

@ -0,0 +1,7 @@
{:paths ["src"]
:deps {}
:tasks
{:requires ([babashka.fs :as fs])
run {:doc "Run commitly"
:task (load-file "commitly")}}}

116
commitly Executable file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env bb
(ns commitly.core
(:require [babashka.process :as process]
[babashka.fs :as fs]
[clojure.string :as str]))
(defn git-repo? [dir]
"Check if directory is a git repository"
(fs/exists? (fs/path dir ".git")))
(defn has-changes? [repo-path]
"Check if git repository has uncommitted changes"
(let [result (process/shell {:dir (str repo-path)
:out :string
:continue true}
"git status --porcelain")]
(not (str/blank? (:out result)))))
(defn find-subrepos [parent-dir]
"Find all git repositories in parent directory"
(->> (fs/list-dir parent-dir)
(filter fs/directory?)
(filter git-repo?)
(map str)))
(defn commit-changes [repo-path message]
"Commit all changes in repository with given message"
(try
(let [add-result (process/shell {:dir repo-path
:out :string
:err :string
:continue true}
"git add -A")
commit-result (process/shell {:dir repo-path
:out :string
:err :string
:continue true}
"git commit -m" message)]
(if (zero? (:exit commit-result))
{:success true :repo repo-path}
{:success false :repo repo-path :error (:err commit-result)}))
(catch Exception e
{:success false :repo repo-path :error (.getMessage e)})))
(defn push-changes [repo-path]
"Push committed changes to remote"
(try
(let [push-result (process/shell {:dir repo-path
:out :string
:err :string
:continue true}
"git push")]
(if (zero? (:exit push-result))
{:success true :repo repo-path}
{:success false :repo repo-path :error (:err push-result)}))
(catch Exception e
{:success false :repo repo-path :error (.getMessage e)})))
(defn commitly [commit-message & {:keys [push?]}]
"Main function: commit changes across all modified subrepos"
(let [current-dir (fs/cwd)
subrepos (find-subrepos current-dir)
modified-repos (filter has-changes? subrepos)]
(when (empty? modified-repos)
(println "No repositories with changes found.")
(System/exit 0))
(println (format "Found %d repositories with changes:" (count modified-repos)))
(doseq [repo modified-repos]
(println (format " - %s" (fs/file-name repo))))
(println "\nCommitting changes...")
(let [commit-results (map #(commit-changes % commit-message) modified-repos)]
(doseq [result commit-results]
(if (:success result)
(println (format "✓ %s" (fs/file-name (:repo result))))
(println (format "✗ %s: %s" (fs/file-name (:repo result)) (:error result)))))
(let [failed (filter #(not (:success %)) commit-results)]
(when (seq failed)
(println (format "\n%d repositories failed to commit" (count failed)))
(System/exit 1)))
(when push?
(println "\nPushing changes...")
(let [committed-repos (map :repo (filter :success commit-results))
push-results (map push-changes committed-repos)]
(doseq [result push-results]
(if (:success result)
(println (format "✓ %s" (fs/file-name (:repo result))))
(println (format "✗ %s: %s" (fs/file-name (:repo result)) (:error result)))))
(let [failed (filter #(not (:success %)) push-results)]
(when (seq failed)
(println (format "\n%d repositories failed to push" (count failed)))
(System/exit 1))))))))
;; CLI entry point
(defn -main [& args]
(if (empty? args)
(do
(println "Usage: commitly [-p] <commit-message>")
(println " -p Push changes after committing")
(System/exit 1))
(let [push? (some #(= "-p" %) args)
message-args (remove #(= "-p" %) args)
commit-message (str/join " " message-args)]
(when (str/blank? commit-message)
(println "Error: commit message cannot be empty")
(System/exit 1))
(commitly commit-message :push? push?))))
(when (= *file* (System/getProperty "babashka.file"))
(apply -main *command-line-args*))