A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

243 行
9.4 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2012 by 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. __all__ = ["Git"]
  26. class Git(Command):
  27. """Commands to interface with the bot's git repository; use '!git' for a
  28. sub-command list."""
  29. name = "git"
  30. def setup(self):
  31. try:
  32. self.repos = self.config.commands[self.name]["repos"]
  33. except KeyError:
  34. self.repos = None
  35. def process(self, data):
  36. self.data = data
  37. if data.host not in self.config.irc["permissions"]["owners"]:
  38. msg = "you must be a bot owner to use this command."
  39. self.reply(data, msg)
  40. return
  41. if not data.args or data.args[0] == "help":
  42. self.do_help()
  43. return
  44. if not self.repos:
  45. self.reply(data, "no repos are specified in the config file.")
  46. return
  47. command = data.args[0]
  48. try:
  49. repo_name = data.args[1]
  50. except IndexError:
  51. repos = self.get_repos()
  52. msg = "which repo do you want to work with (options are {0})?"
  53. self.reply(data, msg.format(repos))
  54. return
  55. if repo_name not in self.repos:
  56. repos = self.get_repos()
  57. msg = "repository must be one of the following: {0}."
  58. self.reply(data, msg.format(repos))
  59. return
  60. self.repo = git.Repo(self.repos[repo_name])
  61. if command == "branch":
  62. self.do_branch()
  63. elif command == "branches":
  64. self.do_branches()
  65. elif command == "checkout":
  66. self.do_checkout()
  67. elif command == "delete":
  68. self.do_delete()
  69. elif command == "pull":
  70. self.do_pull()
  71. elif command == "status":
  72. self.do_status()
  73. else: # They asked us to do something we don't know
  74. msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0])
  75. self.reply(data, msg)
  76. def get_repos(self):
  77. data = self.repos.iteritems()
  78. repos = ["\x0302{0}\x0301 ({1})".format(k, v) for k, v in data]
  79. return ", ".join(repos)
  80. def get_remote(self):
  81. try:
  82. remote_name = self.data.args[2]
  83. except IndexError:
  84. remote_name = "origin"
  85. try:
  86. return getattr(self.repo.remotes, remote_name)
  87. except AttributeError:
  88. msg = "unknown remote: \x0302{0}\x0301.".format(remote_name)
  89. self.reply(self.data, msg)
  90. def get_time_since(self, date):
  91. diff = time.mktime(time.gmtime()) - date
  92. if diff < 60:
  93. return "{0} seconds".format(int(diff))
  94. if diff < 60 * 60:
  95. return "{0} minutes".format(int(diff / 60))
  96. if diff < 60 * 60 * 24:
  97. return "{0} hours".format(int(diff / 60 / 60))
  98. return "{0} days".format(int(diff / 60 / 60 / 24))
  99. def do_help(self):
  100. """Display all commands."""
  101. help = {
  102. "branch": "get current branch",
  103. "branches": "get all branches",
  104. "checkout": "switch branches",
  105. "delete": "delete an old branch",
  106. "pull": "update everything from the remote server",
  107. "status": "check if we are up-to-date",
  108. }
  109. subcommands = ""
  110. for key in sorted(help.keys()):
  111. subcommands += "\x0303{0}\x0301 ({1}), ".format(key, help[key])
  112. subcommands = subcommands[:-2] # Trim last comma and space
  113. msg = "sub-commands are: {0}; repos are: {1}. Syntax: !git \x0303subcommand\x0301 \x0302repo\x0301."
  114. self.reply(self.data, msg.format(subcommands, self.get_repos()))
  115. def do_branch(self):
  116. """Get our current branch."""
  117. branch = self.repo.active_branch.name
  118. msg = "currently on branch \x0302{0}\x0301.".format(branch)
  119. self.reply(self.data, msg)
  120. def do_branches(self):
  121. """Get a list of branches."""
  122. branches = [branch.name for branch in self.repo.branches]
  123. msg = "branches: \x0302{0}\x0301.".format(", ".join(branches))
  124. self.reply(self.data, msg)
  125. def do_checkout(self):
  126. """Switch branches."""
  127. try:
  128. target = self.data.args[2]
  129. except IndexError: # No branch name provided
  130. self.reply(self.data, "switch to which branch?")
  131. return
  132. current_branch = self.repo.active_branch.name
  133. if target == current_branch:
  134. msg = "already on \x0302{0}\x0301!".format(target)
  135. self.reply(self.data, msg)
  136. return
  137. try:
  138. ref = getattr(self.repo.branches, target)
  139. except AttributeError:
  140. msg = "branch \x0302{0}\x0301 doesn't exist!".format(target)
  141. self.reply(self.data, msg)
  142. else:
  143. ref.checkout()
  144. ms = "switched from branch \x0302{0}\x0301 to \x0302{1}\x0301."
  145. msg = ms.format(current_branch, target)
  146. self.reply(self.data, msg)
  147. log = "{0} checked out branch {1} of {2}"
  148. logmsg = log.format(self.data.nick, target, self.repo.working_dir)
  149. self.logger.info(logmsg)
  150. def do_delete(self):
  151. """Delete a branch, while making sure that we are not already on it."""
  152. try:
  153. target = self.data.args[2]
  154. except IndexError: # No branch name provided
  155. self.reply(self.data, "delete which branch?")
  156. return
  157. current_branch = self.repo.active_branch.name
  158. if current_branch == target:
  159. msg = "you're currently on this branch; please checkout to a different branch before deleting."
  160. self.reply(self.data, msg)
  161. return
  162. try:
  163. ref = getattr(self.repo.branches, target)
  164. except AttributeError:
  165. msg = "branch \x0302{0}\x0301 doesn't exist!".format(target)
  166. self.reply(self.data, msg)
  167. else:
  168. self.repo.git.branch("-d", ref)
  169. msg = "branch \x0302{0}\x0301 has been deleted locally."
  170. self.reply(self.data, msg.format(target))
  171. log = "{0} deleted branch {1} of {2}"
  172. logmsg = log.format(self.data.nick, target, self.repo.working_dir)
  173. self.logger.info(logmsg)
  174. def do_pull(self):
  175. """Pull from our remote repository."""
  176. branch = self.repo.active_branch.name
  177. msg = "pulling from remote (currently on \x0302{0}\x0301)..."
  178. self.reply(self.data, msg.format(branch))
  179. remote = self.get_remote()
  180. if not remote:
  181. return
  182. result = remote.pull()
  183. updated = [info for info in result if info.flags != info.HEAD_UPTODATE]
  184. if updated:
  185. branches = ", ".join([info.ref.remote_head for info in updated])
  186. msg = "done; updates to \x0302{0}\x0301 (from {1})."
  187. self.reply(self.data, msg.format(branches, remote.url))
  188. log = "{0} pulled {1} of {2} (updates to {3})"
  189. self.logger.info(log.format(self.data.nick, remote.name,
  190. self.repo.working_dir, branches))
  191. else:
  192. self.reply(self.data, "done; no new changes.")
  193. log = "{0} pulled {1} of {2} (no updates)"
  194. self.logger.info(log.format(self.data.nick, remote.name,
  195. self.repo.working_dir))
  196. def do_status(self):
  197. """Check if we have anything to pull."""
  198. remote = self.get_remote()
  199. if not remote:
  200. return
  201. since = self.get_time_since(self.repo.head.object.committed_date)
  202. result = remote.fetch(dry_run=True)
  203. updated = [info for info in result if info.flags != info.HEAD_UPTODATE]
  204. if updated:
  205. branches = ", ".join([info.ref.remote_head for info in updated])
  206. msg = "last local commit was \x02{0}\x0F ago; updates to \x0302{1}\x0301."
  207. self.reply(self.data, msg.format(since, branches))
  208. log = "{0} got status of {1} of {2} (updates to {3})"
  209. self.logger.info(log.format(self.data.nick, remote.name,
  210. self.repo.working_dir, branches))
  211. else:
  212. msg = "last commit was \x02{0}\x0F ago. Local copy is up-to-date with remote."
  213. self.reply(self.data, msg.format(since))
  214. log = "{0} pulled {1} of {2} (no updates)"
  215. self.logger.info(log.format(self.data.nick, remote.name,
  216. self.repo.working_dir))