aboutsummaryrefslogtreecommitdiffstats
path: root/alfred/git.py
diff options
context:
space:
mode:
Diffstat (limited to 'alfred/git.py')
-rw-r--r--alfred/git.py148
1 files changed, 148 insertions, 0 deletions
diff --git a/alfred/git.py b/alfred/git.py
new file mode 100644
index 0000000..107a3b1
--- /dev/null
+++ b/alfred/git.py
@@ -0,0 +1,148 @@
+"""
+Sopel plugin: bare git repo management
+
+ !git list list all repos
+ !git desc <repo> <text> set repo description
+ !git hook <repo> <type> install a named post-receive hook
+ !git remove <repo> remove a repo (confirms first)
+"""
+
+import os
+import shutil
+import subprocess
+
+from sopel import plugin
+
+REPOS = "/home/git/repos"
+HOOKS = "/home/ahmed/scripts/git-hooks"
+
+_pending_remove = {}
+
+
+def _owner(bot, trigger):
+ return trigger.nick == bot.settings.core.owner
+
+
+def _repos():
+ try:
+ return sorted(
+ e for e in os.listdir(REPOS)
+ if os.path.isdir(os.path.join(REPOS, e))
+ )
+ except OSError:
+ return []
+
+
+def _desc(name):
+ path = os.path.join(REPOS, name, "description")
+ try:
+ text = open(path).read().strip()
+ if text.startswith("Unnamed repository"):
+ return ""
+ return text
+ except OSError:
+ return ""
+
+
+def _last_commit(name):
+ try:
+ r = subprocess.run(
+ ["git", "-C", os.path.join(REPOS, name), "log", "-1", "--format=%cr"],
+ capture_output=True, text=True, timeout=5,
+ )
+ return r.stdout.strip() or "no commits"
+ except Exception:
+ return "?"
+
+
+@plugin.command("git")
+def cmd_git(bot, trigger):
+ if not _owner(bot, trigger):
+ return
+
+ args = (trigger.group(2) or "").split()
+ if not args:
+ bot.say("usage: !git <list|desc|hook|remove>")
+ return
+
+ action = args[0]
+
+ if action == "list":
+ repos = _repos()
+ if not repos:
+ bot.say("no repos")
+ return
+ for name in repos:
+ desc = _desc(name)
+ last = _last_commit(name)
+ line = f"{name} ({last})"
+ if desc:
+ line += f" {desc}"
+ bot.say(line)
+
+ elif action == "desc":
+ if len(args) < 3:
+ bot.say("usage: !git desc <repo> <description>")
+ return
+ name, text = args[1], " ".join(args[2:])
+ path = os.path.join(REPOS, name)
+ if not os.path.isdir(path):
+ bot.say(f"{name}: not found")
+ return
+ with open(os.path.join(path, "description"), "w") as f:
+ f.write(text + "\n")
+ bot.say(f"{name}: description updated")
+
+ elif action == "hook":
+ if len(args) < 3:
+ available = [
+ f[len("post-receive-"):-len(".sh")]
+ for f in os.listdir(HOOKS)
+ if f.startswith("post-receive-") and f.endswith(".sh")
+ ] if os.path.isdir(HOOKS) else []
+ bot.say(f"usage: !git hook <repo> <type> available: {', '.join(available) or 'none'}")
+ return
+ name, hooktype = args[1], args[2]
+ repo_path = os.path.join(REPOS, name)
+ if not os.path.isdir(repo_path):
+ bot.say(f"{name}: not found")
+ return
+ src = os.path.join(HOOKS, f"post-receive-{hooktype}.sh")
+ if not os.path.isfile(src):
+ bot.say(f"{hooktype}: hook template not found")
+ return
+ dst = os.path.join(repo_path, "hooks", "post-receive")
+ shutil.copy2(src, dst)
+ os.chmod(dst, 0o755)
+ bot.say(f"{name}: {hooktype} hook installed")
+
+ elif action == "remove":
+ if len(args) < 2:
+ bot.say("usage: !git remove <repo>")
+ return
+ name = args[1]
+ path = os.path.join(REPOS, name)
+ if not os.path.isdir(path):
+ bot.say(f"{name}: not found")
+ return
+ channel = str(trigger.sender)
+ _pending_remove[channel] = path
+ bot.say(f"remove {name}? reply !git yes or !git no")
+
+ elif action == "yes":
+ channel = str(trigger.sender)
+ path = _pending_remove.pop(channel, None)
+ if not path:
+ bot.say("nothing pending")
+ return
+ name = os.path.basename(path)
+ shutil.rmtree(path)
+ bot.say(f"{name}: removed")
+
+ elif action == "no":
+ channel = str(trigger.sender)
+ _pending_remove.pop(channel, None)
+ bot.say("cancelled")
+
+ else:
+ bot.say("usage: !git <list|desc|hook|remove>")