Additional IRC commands and bot tasks for EarwigBot https://en.wikipedia.org/wiki/User:EarwigBot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

241 lines
9.4 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2012 Ben Kurtovic <ben.kurtovic@verizon.net>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22. import time
  23. import git
  24. from earwigbot.commands import Command
  25. class Git(Command):
  26. """Commands to interface with configurable git repositories; use '!git' for
  27. a sub-command list."""
  28. name = "git"
  29. def setup(self):
  30. try:
  31. self.repos = self.config.commands[self.name]["repos"]
  32. except KeyError:
  33. self.repos = None
  34. def process(self, data):
  35. self.data = data
  36. if not self.config.irc["permissions"].is_owner(data):
  37. msg = "You must be a bot owner to use this command."
  38. self.reply(data, msg)
  39. return
  40. if not data.args or data.args[0] == "help":
  41. self.do_help()
  42. return
  43. if not self.repos:
  44. self.reply(data, "No repos are specified in the config file.")
  45. return
  46. command = data.args[0]
  47. try:
  48. repo_name = data.args[1]
  49. except IndexError:
  50. repos = self.get_repos()
  51. msg = "Which repo do you want to work with (options are {0})?"
  52. self.reply(data, msg.format(repos))
  53. return
  54. if repo_name not in self.repos:
  55. repos = self.get_repos()
  56. msg = "Repository must be one of the following: {0}."
  57. self.reply(data, msg.format(repos))
  58. return
  59. self.repo = git.Repo(self.repos[repo_name])
  60. if command == "branch":
  61. self.do_branch()
  62. elif command == "branches":
  63. self.do_branches()
  64. elif command == "checkout":
  65. self.do_checkout()
  66. elif command == "delete":
  67. self.do_delete()
  68. elif command == "pull":
  69. self.do_pull()
  70. elif command == "status":
  71. self.do_status()
  72. else: # They asked us to do something we don't know
  73. msg = "Unknown argument: \x0303{0}\x0F.".format(data.args[0])
  74. self.reply(data, msg)
  75. def get_repos(self):
  76. data = self.repos.iteritems()
  77. repos = ["\x0302{0}\x0F ({1})".format(k, v) for k, v in data]
  78. return ", ".join(repos)
  79. def get_remote(self):
  80. try:
  81. remote_name = self.data.args[2]
  82. except IndexError:
  83. remote_name = "origin"
  84. try:
  85. return getattr(self.repo.remotes, remote_name)
  86. except AttributeError:
  87. msg = "Unknown remote: \x0302{0}\x0F.".format(remote_name)
  88. self.reply(self.data, msg)
  89. def get_time_since(self, date):
  90. diff = time.mktime(time.gmtime()) - date
  91. if diff < 60:
  92. return "{0} seconds".format(int(diff))
  93. if diff < 60 * 60:
  94. return "{0} minutes".format(int(diff / 60))
  95. if diff < 60 * 60 * 24:
  96. return "{0} hours".format(int(diff / 60 / 60))
  97. return "{0} days".format(int(diff / 60 / 60 / 24))
  98. def do_help(self):
  99. """Display all commands."""
  100. help = {
  101. "branch": "get current branch",
  102. "branches": "get all branches",
  103. "checkout": "switch branches",
  104. "delete": "delete an old branch",
  105. "pull": "update everything from the remote server",
  106. "status": "check if we are up-to-date",
  107. }
  108. subcommands = ""
  109. for key in sorted(help.keys()):
  110. subcommands += "\x0303{0}\x0F ({1}), ".format(key, help[key])
  111. subcommands = subcommands[:-2] # Trim last comma and space
  112. msg = "Sub-commands are: {0}; repos are: {1}. Syntax: !git \x0303subcommand\x0F \x0302repo\x0F."
  113. self.reply(self.data, msg.format(subcommands, self.get_repos()))
  114. def do_branch(self):
  115. """Get our current branch."""
  116. branch = self.repo.active_branch.name
  117. msg = "Currently on branch \x0302{0}\x0F.".format(branch)
  118. self.reply(self.data, msg)
  119. def do_branches(self):
  120. """Get a list of branches."""
  121. branches = [branch.name for branch in self.repo.branches]
  122. msg = "Branches: \x0302{0}\x0F.".format(", ".join(branches))
  123. self.reply(self.data, msg)
  124. def do_checkout(self):
  125. """Switch branches."""
  126. try:
  127. target = self.data.args[2]
  128. except IndexError: # No branch name provided
  129. self.reply(self.data, "Wwitch to which branch?")
  130. return
  131. current_branch = self.repo.active_branch.name
  132. if target == current_branch:
  133. msg = "Already on \x0302{0}\x0F!".format(target)
  134. self.reply(self.data, msg)
  135. return
  136. try:
  137. ref = getattr(self.repo.branches, target)
  138. except AttributeError:
  139. msg = "Branch \x0302{0}\x0F doesn't exist!".format(target)
  140. self.reply(self.data, msg)
  141. else:
  142. ref.checkout()
  143. ms = "Switched from branch \x0302{0}\x0F to \x0302{1}\x0F."
  144. msg = ms.format(current_branch, target)
  145. self.reply(self.data, msg)
  146. log = "{0} checked out branch {1} of {2}"
  147. logmsg = log.format(self.data.nick, target, self.repo.working_dir)
  148. self.logger.info(logmsg)
  149. def do_delete(self):
  150. """Delete a branch, while making sure that we are not already on it."""
  151. try:
  152. target = self.data.args[2]
  153. except IndexError: # No branch name provided
  154. self.reply(self.data, "Delete which branch?")
  155. return
  156. current_branch = self.repo.active_branch.name
  157. if current_branch == target:
  158. msg = "You're currently on this branch; please checkout to a different branch before deleting."
  159. self.reply(self.data, msg)
  160. return
  161. try:
  162. ref = getattr(self.repo.branches, target)
  163. except AttributeError:
  164. msg = "Branch \x0302{0}\x0F doesn't exist!".format(target)
  165. self.reply(self.data, msg)
  166. else:
  167. self.repo.git.branch("-d", ref)
  168. msg = "Branch \x0302{0}\x0F has been deleted locally."
  169. self.reply(self.data, msg.format(target))
  170. log = "{0} deleted branch {1} of {2}"
  171. logmsg = log.format(self.data.nick, target, self.repo.working_dir)
  172. self.logger.info(logmsg)
  173. def do_pull(self):
  174. """Pull from our remote repository."""
  175. branch = self.repo.active_branch.name
  176. msg = "Pulling from remote (currently on \x0302{0}\x0F)..."
  177. self.reply(self.data, msg.format(branch))
  178. remote = self.get_remote()
  179. if not remote:
  180. return
  181. result = remote.pull()
  182. updated = [info for info in result if info.flags != info.HEAD_UPTODATE]
  183. if updated:
  184. branches = ", ".join([info.ref.remote_head for info in updated])
  185. msg = "Done; updates to \x0302{0}\x0F (from {1})."
  186. self.reply(self.data, msg.format(branches, remote.url))
  187. log = "{0} pulled {1} of {2} (updates to {3})"
  188. self.logger.info(log.format(self.data.nick, remote.name,
  189. self.repo.working_dir, branches))
  190. else:
  191. self.reply(self.data, "Done; no new changes.")
  192. log = "{0} pulled {1} of {2} (no updates)"
  193. self.logger.info(log.format(self.data.nick, remote.name,
  194. self.repo.working_dir))
  195. def do_status(self):
  196. """Check if we have anything to pull."""
  197. remote = self.get_remote()
  198. if not remote:
  199. return
  200. since = self.get_time_since(self.repo.head.object.committed_date)
  201. result = remote.fetch(dry_run=True)
  202. updated = [info for info in result if info.flags != info.HEAD_UPTODATE]
  203. if updated:
  204. branches = ", ".join([info.ref.remote_head for info in updated])
  205. msg = "Last local commit was \x02{0}\x0F ago; updates to \x0302{1}\x0F."
  206. self.reply(self.data, msg.format(since, branches))
  207. log = "{0} got status of {1} of {2} (updates to {3})"
  208. self.logger.info(log.format(self.data.nick, remote.name,
  209. self.repo.working_dir, branches))
  210. else:
  211. msg = "Last commit was \x02{0}\x0F ago. Local copy is up-to-date with remote."
  212. self.reply(self.data, msg.format(since))
  213. log = "{0} pulled {1} of {2} (no updates)"
  214. self.logger.info(log.format(self.data.nick, remote.name,
  215. self.repo.working_dir))