Guest User

Untitled

a guest
Jan 22nd, 2018
68
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.71 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. # p4.py - p4 offline caching
  3. #
  4. # Copyright (c) 2017 Arvid Gerstmann
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining a copy
  7. # of this software and associated documentation files (the "Software"), to deal
  8. # in the Software without restriction, including without limitation the rights
  9. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. # copies of the Software, and to permit persons to whom the Software is
  11. # furnished to do so, subject to the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included in all
  14. # copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. # SOFTWARE.
  23. #
  24. #
  25. # DESCRIPTION:
  26. # p4.py will act as a wrapper between you and p4. When you have a working
  27. # network connection, p4.py will forward all passed arguments unchanged to p4.
  28. # In the case you have no network, p4.py will cache all 'add', 'edit',
  29. # or 'delete' commands.
  30. # Once you invoke p4.py with a working connection again, the cached commands
  31. # will be replayed and added to p4. This process can be done manually by
  32. # invoking 'p4.py replay'.
  33. #
  34. #
  35. # INSTALLATION:
  36. # - Copy the 'p4.py' file into your path.
  37. # - Make sure the 'p4' executable is in your path
  38. # - Rename 'p4' to '_p4', symlink 'p4.py' to 'p4' (optional)
  39. #
  40. #
  41. # USAGE:
  42. # p4.py - offline caching
  43. #
  44. # Usage: p4.py [COMMAND] FILE...
  45. #
  46. # Cached commands:
  47. # add Open files for adding to the depot
  48. # edit Open existing files for editing
  49. # delete Open existing files for removal from the depot
  50. # revert Revert open files and restore originals to workspace
  51. #
  52. # Extended commands:
  53. # replay Replay all offline cached commands
  54. #
  55. #
  56. # REVISION HISTORY:
  57. # 1.0.1 (2018-01-12)
  58. # Use google.com as network indicator
  59. # 1.0.0 (2018-01-12)
  60. # initial release
  61. #
  62.  
  63. import os
  64. import sys
  65. import subprocess
  66. import platform
  67. import socket
  68. import pickle
  69.  
  70.  
  71. # =============================================================================
  72. # Helper
  73. # =============================================================================
  74. def has_internet():
  75. try:
  76. host = socket.gethostbyname("google.com")
  77. s = socket.create_connection((host, 80), 2)
  78. return True
  79. except:
  80. pass
  81. return False
  82.  
  83. def is_posix():
  84. return not platform.system() == "Windows"
  85.  
  86. def find_root(anchor=".p4ignore"):
  87. cur_dir = os.getcwd()
  88. while True:
  89. file_list = os.listdir(cur_dir)
  90. parent_dir = os.path.dirname(cur_dir)
  91. if anchor in file_list:
  92. return cur_dir
  93. else:
  94. if cur_dir == parent_dir: break
  95. else: cur_dir = parent_dir
  96. return os.getcwd()
  97.  
  98. def to_relative_path(path):
  99. if type(path).__name__ == "CachedFile":
  100. return os.path.relpath(path.path)
  101. return os.path.relpath(path)
  102.  
  103. def set_readwrite(path):
  104. if is_posix():
  105. p = subprocess.run(["chmod", "+w", path])
  106. else:
  107. p = subprocess.run(["attrib", "-r", path])
  108. if p.returncode != 0:
  109. raise IOError("Could not set file read/write")
  110.  
  111. def set_readonly(path):
  112. if is_posix():
  113. p = subprocess.run(["chmod", "-w", path])
  114. else:
  115. p = subprocess.run(["attrib", "+r", path])
  116. if p.returncode != 0:
  117. raise IOError("Could not set file read-only")
  118.  
  119.  
  120. # =============================================================================
  121. # P4 forwarding
  122. # =============================================================================
  123. def forward_to_p4(argc, argv):
  124. args = []
  125. args.extend(["p4"])
  126. args.extend(argv[1:])
  127. p = subprocess.run(args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
  128. sys.stdout.write(p.stdout.decode("utf-8"))
  129. sys.stdout.flush()
  130. return p.returncode
  131.  
  132.  
  133. # =============================================================================
  134. # Offline caching
  135. # =============================================================================
  136. class CachedFile:
  137. def __init__(self, path):
  138. with open(path, "rb") as f:
  139. self.content = f.read()
  140. self.path = os.path.abspath(path)
  141. set_readwrite(self.path)
  142.  
  143. def __eq__(self, rhs):
  144. if type(rhs).__name__ == "CachedFile":
  145. return self.path == rhs.path
  146. return self.path == rhs
  147.  
  148.  
  149. class Cache:
  150. def __init__(self):
  151. self.added_files = []
  152. self.opened_files = []
  153. self.deleted_files = []
  154.  
  155. @staticmethod
  156. def load_from_file(path):
  157. with open(path, "rb") as f:
  158. return pickle.load(f)
  159.  
  160. def save_to_file(self, path):
  161. with open(path, "wb") as f:
  162. pickle.dump(self, f)
  163.  
  164. def is_open(self, path):
  165. return path in self.opened_files
  166.  
  167. def open_file(self, path):
  168. path = os.path.abspath(path)
  169. if not path in self.opened_files:
  170. self.opened_files.append(CachedFile(path))
  171. print("{} - opened for edit".format(to_relative_path(path)))
  172. elif path in self.opened_files:
  173. print("{} - currently opened for edit".format(to_relative_path(path)))
  174. elif path in self.added_files:
  175. print("{} - currently opened for add".format(to_relative_path(path)))
  176. return 0
  177.  
  178. def revert_file(self, path):
  179. path = os.path.abspath(path)
  180. if path in self.opened_files:
  181. os.remove(path)
  182. with open(path, "wb") as f:
  183. idx = self.opened_files.index(path)
  184. f.write(self.opened_files[idx].content)
  185. self.opened_files.remove(path)
  186. set_readonly(path)
  187. print("{} - was edit, reverted".format(to_relative_path(path)))
  188. elif path in self.added_files:
  189. # Do not actually delete the file, as a safety-measurement.
  190. self.added_files.remove(path)
  191. print("{} - was add, abandoned".format(to_relative_path(path)))
  192. elif path in self.deleted_files:
  193. with open(path, "wb") as f:
  194. idx = self.deleted_files.index(path)
  195. f.write(self.deleted_files[idx].content)
  196. self.deleted_files.remove(path)
  197. set_readonly(path)
  198. print("{} - was delete, reverted".format(to_relative_path(path)))
  199. return 0
  200.  
  201. def add_file(self, path):
  202. path = os.path.abspath(path)
  203. if path in self.deleted_files:
  204. print("{} - can't add (already opened for delete)"
  205. .format(to_relative_path(path)))
  206. elif path in self.opened_files:
  207. print("{} - can't add (already opened for edit)"
  208. .format(to_relative_path(path)))
  209. elif not path in self.added_files:
  210. self.added_files.append(path)
  211. print("{} - opened for add".format(to_relative_path(path)))
  212. else:
  213. print("{} - currently opened for add".format(to_relative_path(path)))
  214. return 0
  215.  
  216. def delete_file(self, path):
  217. path = os.path.abspath(path)
  218. if path in self.added_files or path in self.opened_files:
  219. print("{} - currently opened for delete"
  220. .format(to_relative_path(path)))
  221. elif not path in self.deleted_files:
  222. self.deleted_files.append(CachedFile(path))
  223. print("{} - opened for delete".format(to_relative_path(path)))
  224. os.remove(path)
  225. return 0
  226.  
  227.  
  228. def cache_args(argc, argv, cache_path):
  229. args = argv[1:]
  230.  
  231. # If we have less than two arguments, we can't work our magic.
  232. if argc < 2:
  233. return forward_to_p4(argc, argv)
  234.  
  235. if os.path.isfile(cache_path):
  236. cached = Cache.load_from_file(cache_path)
  237. else:
  238. cached = Cache()
  239.  
  240. def status():
  241. if len(cached.added_files) == 0 and \
  242. len(cached.opened_files) == 0 and \
  243. len(cached.deleted_files) == 0:
  244. print("no files opened for edit, add or delete")
  245. else:
  246. for i in cached.added_files:
  247. print("{} - add".format(to_relative_path(i)))
  248. for i in cached.opened_files:
  249. print("{} - edit".format(to_relative_path(i)))
  250. for i in cached.deleted_files:
  251. print("{} - deleted".format(to_relative_path(i)))
  252. return 0
  253.  
  254. returncode = -1
  255. if args[0] == "add":
  256. returncode = cached.add_file(args[1])
  257. elif args[0] == "edit":
  258. returncode = cached.open_file(args[1])
  259. elif args[0] == "delete":
  260. returncode = cached.delete_file(args[1])
  261. elif args[0] == "revert":
  262. returncode = cached.revert_file(args[1])
  263. elif args[0] == "status":
  264. returncode = status()
  265.  
  266. # Save to disk!
  267. if returncode != -1:
  268. cached.save_to_file(cache_path)
  269. return returncode
  270.  
  271. return forward_to_p4(argc, argv)
  272.  
  273.  
  274. # =============================================================================
  275. # Replaying of offline cache
  276. # =============================================================================
  277. def replay_offline_cache(cache_path):
  278. def p4_cmd(cmd, args):
  279. argv = [ "p4", cmd ]
  280. if len(args) < 1:
  281. return 0
  282. argv.extend(args)
  283. p = subprocess.run(argv)
  284. return p.returncode
  285.  
  286. cached = Cache.load_from_file(cache_path)
  287. if p4_cmd("add", cached.added_files) != 0:
  288. return 1
  289. if p4_cmd("edit", cached.opened_files) != 0:
  290. return 1
  291. if p4_cmd("delete", cached.deleted_files) != 0:
  292. return 1
  293. os.remove(cache_path)
  294. return 0
  295.  
  296.  
  297. # =============================================================================
  298. # Entry Point
  299. # =============================================================================
  300. def main(argc, argv):
  301. root = find_root()
  302. # TODO: Add a config file, to allow configuring this?
  303. # TODO: Add a 'help' command? Intercept 'p4 help' and inject 'p4 help replay'.
  304. # As well as intercepting 'p4 help replay' and showing the help from above.
  305. cache_path = os.path.abspath(os.path.join(root, ".p4cache"))
  306.  
  307. # Manually replaying the offline cached commands.
  308. if argc > 1 and argv[1] == "replay":
  309. return replay_offline_cache(cache_path)
  310.  
  311. # Detect whether we have network, forward to p4 if yes, cache otherwise.
  312. if has_internet():
  313. if os.path.isfile(cache_path):
  314. replay_offline_cache(cache_path)
  315. return forward_to_p4(argc, argv)
  316. else:
  317. return cache_args(argc, argv, cache_path)
  318.  
  319. if __name__ == "__main__":
  320. sys.exit(main(len(sys.argv), sys.argv))
Add Comment
Please, Sign In to add comment