diff --git a/earwigbot/commands/git.py b/earwigbot/commands/git.py index c22d719..fef1ff0 100644 --- a/earwigbot/commands/git.py +++ b/earwigbot/commands/git.py @@ -20,9 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import shlex -import subprocess -import re +import time + +import git from earwigbot.commands import BaseCommand @@ -30,6 +30,10 @@ class Command(BaseCommand): """Commands to interface with the bot's git repository; use '!git' for a sub-command list.""" name = "git" + repos = { + "core": "/home/earwig/git/earwigbot", + "plugins": "/home/earwig/git/earwigbot-plugins", + } def process(self, data): self.data = data @@ -37,43 +41,66 @@ class Command(BaseCommand): msg = "you must be a bot owner to use this command." self.reply(data, msg) return - - if not data.args: + if not data.args or data.args[0] == "help": self.do_help() return - if data.args[0] == "help": - self.do_help() + command = data.args[0] + try: + repo_name = data.args[1] + except IndexError: + repos = self.get_repos() + msg = "which repo do you want to work with (options are {0})?" + self.reply(data, msg.format(repos)) + return + if repo_name not in self.repos: + repos = self.get_repos() + msg = "repository must be one of the following: {0}" + self.reply(data, msg.format(repos)) + return + self.repo = git.Repo(self.repos[repo_name]) - elif data.args[0] == "branch": + if command == "branch": self.do_branch() - - elif data.args[0] == "branches": + elif command == "branches": self.do_branches() - - elif data.args[0] == "checkout": + elif command == "checkout": self.do_checkout() - - elif data.args[0] == "delete": + elif command == "delete": self.do_delete() - - elif data.args[0] == "pull": + elif command == "pull": self.do_pull() - - elif data.args[0] == "status": + elif command == "status": self.do_status() - else: # They asked us to do something we don't know msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0]) self.reply(data, msg) - def exec_shell(self, command): - """Execute a shell command and get the output.""" - command = shlex.split(command) - result = subprocess.check_output(command, stderr=subprocess.STDOUT) - if result: - result = result[:-1] # Strip newline - return result + def get_repos(self): + data = self.repos.iteritems() + repos = ["\x0302{0}\x0301 ({1})".format(k, v) for k, v in data] + return ", ".join(repos) + + def get_remote(self): + try: + remote_name = self.data.args[2] + except IndexError: + remote_name = "origin" + try: + return getattr(self.repo.remotes, remote_name) + except AttributeError: + msg = "unknown remote: \x0302{0}\x0301".format(remote_name) + self.reply(self.data, msg) + + def get_time_since(self, date): + diff = time.mktime(time.gmtime()) - date + if diff < 60: + return "{0} seconds".format(int(diff)) + if diff < 60 * 60: + return "{0} minutes".format(int(diff / 60)) + if diff < 60 * 60 * 24: + return "{0} hours".format(int(diff / 60 / 60)) + return "{0} days".format(int(diff / 60 / 60 / 24)) def do_help(self): """Display all commands.""" @@ -85,110 +112,124 @@ class Command(BaseCommand): "pull": "update everything from the remote server", "status": "check if we are up-to-date", } - msg = "" + subcommands = "" for key in sorted(help.keys()): - msg += "\x0303{0}\x0301 ({1}), ".format(key, help[key]) - msg = msg[:-2] # Trim last comma and space - self.reply(self.data, "sub-commands are: {0}.".format(msg)) + subcommands += "\x0303{0}\x0301 ({1}), ".format(key, help[key]) + subcommands = subcommands[:-2] # Trim last comma and space + msg = "sub-commands are: {0}; repos are: {1}. Syntax: !git \x0303subcommand\x0301 \x0302repo\x0301." + self.reply(self.data, msg.format(subcommands, self.get_repos())) def do_branch(self): """Get our current branch.""" - branch = self.exec_shell("git name-rev --name-only HEAD") + branch = self.repo.active_branch.name msg = "currently on branch \x0302{0}\x0301.".format(branch) self.reply(self.data, msg) def do_branches(self): """Get a list of branches.""" - branches = self.exec_shell("git branch") - # Remove extraneous characters: - branches = branches.replace('\n* ', ', ') - branches = branches.replace('* ', ' ') - branches = branches.replace('\n ', ', ') - branches = branches.strip() - msg = "branches: \x0302{0}\x0301.".format(branches) + branches = [branch.name for branch in self.repo.branches] + msg = "branches: \x0302{0}\x0301.".format(", ".join(branches)) self.reply(self.data, msg) def do_checkout(self): """Switch branches.""" try: - branch = self.data.args[1] - except IndexError: # no branch name provided + target = self.data.args[2] + except IndexError: # No branch name provided self.reply(self.data, "switch to which branch?") return - current_branch = self.exec_shell("git name-rev --name-only HEAD") + current_branch = self.repo.active_branch.name + if target == current_branch: + msg = "already on \x0302{0}\x0301!".format(target) + self.reply(self.data, msg) + return try: - result = self.exec_shell("git checkout %s" % branch) - if "Already on" in result: - msg = "already on \x0302{0}\x0301!".format(branch) - self.reply(self.data, msg) - else: - ms = "switched from branch \x0302{1}\x0301 to \x0302{1}\x0301." - msg = ms.format(current_branch, branch) - self.reply(self.data, msg) - - except subprocess.CalledProcessError: - # Git couldn't switch branches; assume the branch doesn't exist: - msg = "branch \x0302{0}\x0301 doesn't exist!".format(branch) + ref = getattr(self.repo.branches, target) + except AttributeError: + msg = "branch \x0302{0}\x0301 doesn't exist!".format(target) self.reply(self.data, msg) + else: + ref.checkout() + ms = "switched from branch \x0302{1}\x0301 to \x0302{1}\x0301." + msg = ms.format(current_branch, target) + self.reply(self.data, msg) + log = "{0} checked out branch {1} of {2}" + logmsg = log.format(self.data.nick, target, self.repo.working_dir) + self.logger.info(logmsg) def do_delete(self): """Delete a branch, while making sure that we are not already on it.""" try: - delete_branch = self.data.args[1] - except IndexError: # no branch name provided + target = self.data.args[2] + except IndexError: # No branch name provided self.reply(self.data, "delete which branch?") return - current_branch = self.exec_shell("git name-rev --name-only HEAD") - - if current_branch == delete_branch: + current_branch = self.repo.active_branch.name + if current_branch == target: msg = "you're currently on this branch; please checkout to a different branch before deleting." self.reply(self.data, msg) return try: - self.exec_shell("git branch -d %s" % delete_branch) - msg = "branch \x0302{0}\x0301 has been deleted locally." - self.reply(self.data, msg.format(delete_branch)) - except subprocess.CalledProcessError: - # Git couldn't switch branches; assume the branch doesn't exist: - msg = "branch \x0302{0}\x0301 doesn't exist!".format(delete_branch) + ref = getattr(self.repo.branches, target) + except AttributeError: + msg = "branch \x0302{0}\x0301 doesn't exist!".format(target) self.reply(self.data, msg) + else: + self.repo.git.branch("-d", ref) + msg = "branch \x0302{0}\x0301 has been deleted locally." + self.reply(self.data, msg.format(target)) + log = "{0} deleted branch {1} of {2}" + logmsg = log.format(self.data.nick, target, self.repo.working_dir) + self.logger.info(logmsg) def do_pull(self): """Pull from our remote repository.""" - branch = self.exec_shell("git name-rev --name-only HEAD") + branch = self.repo.active_branch.name msg = "pulling from remote (currently on \x0302{0}\x0301)..." self.reply(self.data, msg.format(branch)) - result = self.exec_shell("git pull") - - if "Already up-to-date." in result: - self.reply(self.data, "done; no new changes.") + remote = self.get_remote() + if not remote: + return + result = remote.pull() + updated = [info for info in result if info.flags != info.HEAD_UPTODATE] + + if updated: + branches = ", ".join([info.ref.remote_head for info in updated]) + msg = "done; updates to \x0302{0}\x0301 (from {1})." + self.reply(self.data, msg.format(branches, remote.url)) + log = "{0} pulled {1} of {2} (updates to {3})" + self.logger.info(log.format(self.data.nick, remote.name, + self.repo.working_dir, branches)) else: - regex = "\s*((.*?)\sfile(.*?)tions?\(-\))" - changes = re.findall(regex, result)[0][0] - try: - cmnd_remt = "git config --get branch.{0}.remote".format(branch) - remote = self.exec_shell(cmnd_remt) - cmnd_url = "git config --get remote.{0}.url".format(remote) - url = self.exec_shell(cmnd_url) - msg = "done; {0} [from {1}].".format(changes, url) - self.reply(self.data, msg) - except subprocess.CalledProcessError: - # Something in .git/config is not specified correctly, so we - # cannot get the remote's URL. However, pull was a success: - self.reply(self.data, "done; %s." % changes) + self.reply(self.data, "done; no new changes.") + log = "{0} pulled {1} of {2} (no updates)" + self.logger.info(log.format(self.data.nick, remote.name, + self.repo.working_dir)) def do_status(self): - """Check whether we have anything to pull.""" - last = self.exec_shell('git log -n 1 --pretty="%ar"') - result = self.exec_shell("git fetch --dry-run") - if not result: # Nothing was fetched, so remote and local are equal - msg = "last commit was {0}. Local copy is \x02up-to-date\x0F with remote." - self.reply(self.data, msg.format(last)) + """Check if we have anything to pull.""" + remote = self.get_remote() + if not remote: + return + since = self.get_time_since(self.repo.head.object.committed_date) + result = remote.fetch(dry_run=True) + updated = [info for info in result if info.flags != info.HEAD_UPTODATE] + + if updated: + branches = ", ".join([info.ref.remote_head for info in updated]) + msg = "last local commit was \x02{0}\x0F ago; updates to \x0302{1}\x0301." + self.reply(self.data, msg.format(since, branches)) + log = "{0} got status of {1} of {2} (updates to {3})" + self.logger.info(log.format(self.data.nick, remote.name, + self.repo.working_dir, branches)) else: - msg = "last local commit was {0}. Remote is \x02ahead\x0F of local copy." - self.reply(self.data, msg.format(last)) + msg = "last commit was \x02{0}\x0F ago. Local copy is up-to-date with remote." + self.reply(self.data, msg.format(since)) + log = "{0} pulled {1} of {2} (no updates)" + self.logger.info(log.format(self.data.nick, remote.name, + self.repo.working_dir))