#!/usr/bin/python3 # coding=utf-8 # # See LICENSE. # # Copy me if you can. # by 20h # import os import sys import re import getopt import click import codecs from subprocess import Popen, PIPE DEFAULTBASE = "~/.psw" class git(object): def __init__(self, base=None): if base == None: base = DEFAULTBASE self.base = os.path.expanduser(base) if not os.path.exists(self.base): os.makedirs(self.base, 0o750) if self.base[-1] != os.sep: self.pages = "%s%s" % (self.base, os.sep) else: self.pages = self.base self.keycache = None """ I/O helper functions. """ def getfile(self, fi): fd = codecs.open(fi, mode="r", encoding="utf-8") text = fd.read() fd.close() return text def putfile(self, fi, content): os.makedirs(os.path.dirname(fi), exist_ok=True) fd = codecs.open(fi, mode="w+", encoding="utf-8") fd.write(content) fd.close() return None def mkpath(self, path, fi=None): if path[-1] != os.sep: path = "%s%s" % (path, os.sep) if fi != None: path = "%s%s" % (path, fi) return path def recursedir(self, path): subdirs = [] files = os.listdir(path) files.sort() for f in files: npath = self.mkpath(path, f) if os.path.isfile(npath) or \ os.path.islink(npath): yield npath else: subdirs.append(f) for s in subdirs: npath = self.mkpath(path, s) if s == ".git": continue for j in self.recursedir(npath): yield j def makesearchlist(self, fun, path): li = [] npath = self.mkpath(path) for i in fun(path): i = i.replace(npath, "") i = i.removesuffix(".md") li.append(i) return li """ Git command functions. """ def git(self, args): gitdir = "%s.git" % (self.pages) workdir = self.pages cmd = ["git", "--git-dir=%s" % gitdir, "--work-tree=%s" % workdir] + args p = Popen(cmd, stdout=PIPE, stderr=PIPE) result = p.stdout.read().decode("utf-8", errors="replace") return [result, p.wait()] def gitpath(self, file): return "%s%s.md" % (self.pages, file) def init(self): self.git(["init"]) def add(self, file): self.git(["add", file]) def rm(self, file): self.git(["rm", file]) def mv(self, old, new): self.git(["mv", old, new]) def commit(self, msg): self.git(["commit", "--allow-empty", "--no-verify", "--message=%s" % msg, "--author=psw "]) self.git(["gc"]) def log(self, page): changes = [] if page == "": extra = [] else: extra = ["--", self.gitpath(page)] sep = "\x01" (result, status) = self.git( ["log", "--pretty=format:%%H%s%%T%s%%an%s%%ae%s%%aD%s%%s" % ((sep,) * 5)] + extra ) for line in result.splitlines(): if not line.strip(): continue entries = line.split(sep) if len(entries) < 6: continue change = {} change["commit"] = entries[0] change["author"] = entries[2] change["email"] = entries[3] change["date"] = entries[4] change["message"] = entries[5] try: (task, cpage) = entries[5].split(" ", 1) except ValueError: cpage = page change["page"] = cpage changes.append(change) return changes def showcommit(self, page, commit): (file, status) = self.git(["cat-file", "-p", "%s:%s.md" % (commit, page)]) return file """ Dictionary abstraction functions. """ def __setitem__(self, page, value): file = self.gitpath(page) needadd = False if os.path.exists(file) == False: needadd = True self.putfile(file, value) self.add(file) if needadd: self.commit("Added: %s" % (page)) else: self.commit("Changed: %s" % (page)) def __delitem__(self, page): file = self.gitpath(page) os.remove(file) self.rm(file) self.commit("Deleted: %s" % (page)) def __getitem__(self, page): try: return self.getfile(self.gitpath(page)) except IOError as err: return "" def getlog(self, page): log = self.log(page) ret = "" for i in log: ret += "%s %s %s %s %s %s\n" % (i["commit"], i["author"], i["email"], i["date"], i["page"], i["message"]) return ret def mkkeycache(self): if self.keycache == None: self.keycache = self.makesearchlist(self.recursedir, self.pages) def keys(self): self.mkkeycache() return self.keycache def __contains__(self, item): return item in list(self.keys()) def search(self, query): try: pattern = re.compile(query) except re.error as e: sys.stderr.write("Invalid regex: %s\n" % e) return [] name_hits = [] content_hits = [] seen = set() for page in list(self.keys()): if pattern.search(page): name_hits.append(page) seen.add(page) for page in list(self.keys()): if page in seen: continue try: content = self.getfile(self.gitpath(page)) except OSError: continue for line in content.splitlines(): if pattern.search(line): content_hits.append("%s: %s" % (page, line.strip())) seen.add(page) break return name_hits + content_hits def move(self, old, new): opath = self.gitpath(old) npath = self.gitpath(new) self.mv(opath, npath) self.commit("Moved: %s -> %s" % (old, new)) def editor(content): try: data = click.edit(content, require_save=True, extension='.md') except click.UsageError: return (1, content) if data == None: return (1, content) return (0, data) def usage(app): app = os.path.basename(app) sys.stderr.write("usage: %s [-oh] [-b base] [[-d|-e|-c|-s|-p] item" \ "|-l|-r old new]\n" % (app)) sys.exit(1) def main(args): try: opts, largs = getopt.getopt(args[1:], "hosplb:sdecr") except getopt.GetoptError as err: print(str(err)) usage(args[0]) dorm = False doedit = False docommit = False dosearch = False dolist = False dorename = False tostdout = False base = DEFAULTBASE for o, a in opts: if o == "-h": usage(args[0]) elif o == "-b": base = a elif o == "-c": docommit = True elif o == "-d": dorm = True elif o == "-e": doedit = True elif o == "-l": dolist = True elif o == "-o": doedit = True tostdout = True elif o == "-p": doedit = True tostdout = True elif o == "-r": dorename = True elif o == "-s": dosearch = True else: assert False, "unhandled option" val = "" if doedit == True or dosearch == True or dorm == True or docommit == True: if len(largs) < 1 and not docommit: usage(args[0]) val = "_".join(largs) elif dorename == True: if len(largs) < 2: usage(args[0]) lgit = git(base) if doedit == True: content = lgit[val] if tostdout == True: sys.stdout.write(content) else: (sts, data) = editor(content) if data == content: print("No changes made. Quitting.") else: print("Some changes made. Committing.") lgit[val] = data elif dorename == True: lgit.move(largs[0], largs[1]) elif dorm == True: del lgit[val] elif docommit == True: commits = lgit.getlog(val) if tostdout == True: sys.stdout.write(commits) else: p = Popen(["less"], stdin=PIPE) p.communicate(input=commits.encode("utf-8")) elif dosearch == True: results = lgit.search(val) for r in results: print(r) elif dolist == True: for k in list(lgit.keys()): print(k) else: usage(args[0]) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))