|
@@ -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)) |