A Python robot that edits Wikipedia and interacts with people over IRC 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.

git.py 9.4 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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 the bot's git repository; use '!git' for a
  27. 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 data.host not in self.config.irc["permissions"]["owners"]:
  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}\x0301.".format(data.args[0])
  74. self.reply(data, msg)
  75. def get_repos(self):
  76. data = self.repos.iteritems()
  77. repos = ["\x0302{0}\x0301 ({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}\x0301.".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}\x0301 ({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\x0301 \x0302repo\x0301."
  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}\x0301.".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}\x0301.".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, "switch to which branch?")
  130. return
  131. current_branch = self.repo.active_branch.name
  132. if target == current_branch:
  133. msg = "already on \x0302{0}\x0301!".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}\x0301 doesn't exist!".format(target)
  140. self.reply(self.data, msg)
  141. else:
  142. ref.checkout()
  143. ms = "switched from branch \x0302{0}\x0301 to \x0302{1}\x0301."
  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}\x0301 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}\x0301 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}\x0301)..."
  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}\x0301 (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}\x0301."
  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))