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.

247 lines
9.2 KiB

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