| @@ -20,9 +20,9 @@ | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # SOFTWARE. | ||||
| import shlex | |||||
| import subprocess | |||||
| import re | |||||
| import time | |||||
| import git | |||||
| from earwigbot.commands import BaseCommand | 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 | """Commands to interface with the bot's git repository; use '!git' for a | ||||
| sub-command list.""" | sub-command list.""" | ||||
| name = "git" | name = "git" | ||||
| repos = { | |||||
| "core": "/home/earwig/git/earwigbot", | |||||
| "plugins": "/home/earwig/git/earwigbot-plugins", | |||||
| } | |||||
| def process(self, data): | def process(self, data): | ||||
| self.data = data | self.data = data | ||||
| @@ -37,43 +41,66 @@ class Command(BaseCommand): | |||||
| msg = "you must be a bot owner to use this command." | msg = "you must be a bot owner to use this command." | ||||
| self.reply(data, msg) | self.reply(data, msg) | ||||
| return | return | ||||
| if not data.args: | |||||
| if not data.args or data.args[0] == "help": | |||||
| self.do_help() | self.do_help() | ||||
| return | 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() | self.do_branch() | ||||
| elif data.args[0] == "branches": | |||||
| elif command == "branches": | |||||
| self.do_branches() | self.do_branches() | ||||
| elif data.args[0] == "checkout": | |||||
| elif command == "checkout": | |||||
| self.do_checkout() | self.do_checkout() | ||||
| elif data.args[0] == "delete": | |||||
| elif command == "delete": | |||||
| self.do_delete() | self.do_delete() | ||||
| elif data.args[0] == "pull": | |||||
| elif command == "pull": | |||||
| self.do_pull() | self.do_pull() | ||||
| elif data.args[0] == "status": | |||||
| elif command == "status": | |||||
| self.do_status() | self.do_status() | ||||
| else: # They asked us to do something we don't know | else: # They asked us to do something we don't know | ||||
| msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0]) | msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0]) | ||||
| self.reply(data, msg) | 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): | def do_help(self): | ||||
| """Display all commands.""" | """Display all commands.""" | ||||
| @@ -85,110 +112,124 @@ class Command(BaseCommand): | |||||
| "pull": "update everything from the remote server", | "pull": "update everything from the remote server", | ||||
| "status": "check if we are up-to-date", | "status": "check if we are up-to-date", | ||||
| } | } | ||||
| msg = "" | |||||
| subcommands = "" | |||||
| for key in sorted(help.keys()): | 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): | def do_branch(self): | ||||
| """Get our current branch.""" | """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) | msg = "currently on branch \x0302{0}\x0301.".format(branch) | ||||
| self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
| def do_branches(self): | def do_branches(self): | ||||
| """Get a list of branches.""" | """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) | self.reply(self.data, msg) | ||||
| def do_checkout(self): | def do_checkout(self): | ||||
| """Switch branches.""" | """Switch branches.""" | ||||
| try: | 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?") | self.reply(self.data, "switch to which branch?") | ||||
| return | 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: | 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) | 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): | def do_delete(self): | ||||
| """Delete a branch, while making sure that we are not already on it.""" | """Delete a branch, while making sure that we are not already on it.""" | ||||
| try: | 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?") | self.reply(self.data, "delete which branch?") | ||||
| return | 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." | msg = "you're currently on this branch; please checkout to a different branch before deleting." | ||||
| self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
| return | return | ||||
| try: | 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) | 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): | def do_pull(self): | ||||
| """Pull from our remote repository.""" | """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)..." | msg = "pulling from remote (currently on \x0302{0}\x0301)..." | ||||
| self.reply(self.data, msg.format(branch)) | 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: | 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): | 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: | 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)) | |||||