From 08e503f968c9290779e6922242e97b66a1e7a1fa Mon Sep 17 00:00:00 2001 From: Adam Jeniski Date: Mon, 5 Jan 2026 02:13:30 -0500 Subject: [PATCH] init commitly --- README.md | 46 ++++++++++++++++++++++ bb.edn | 7 ++++ commitly | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 README.md create mode 100644 bb.edn create mode 100755 commitly diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf27514 --- /dev/null +++ b/README.md @@ -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. diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..5d9fe28 --- /dev/null +++ b/bb.edn @@ -0,0 +1,7 @@ +{:paths ["src"] + :deps {} + :tasks + {:requires ([babashka.fs :as fs]) + + run {:doc "Run commitly" + :task (load-file "commitly")}}} diff --git a/commitly b/commitly new file mode 100755 index 0000000..636b722 --- /dev/null +++ b/commitly @@ -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] ") + (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*))