Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- # p4.py - p4 offline caching
- #
- # Copyright (c) 2017 Arvid Gerstmann
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in all
- # copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- # SOFTWARE.
- #
- #
- # DESCRIPTION:
- # p4.py will act as a wrapper between you and p4. When you have a working
- # network connection, p4.py will forward all passed arguments unchanged to p4.
- # In the case you have no network, p4.py will cache all 'add', 'edit',
- # or 'delete' commands.
- # Once you invoke p4.py with a working connection again, the cached commands
- # will be replayed and added to p4. This process can be done manually by
- # invoking 'p4.py replay'.
- #
- #
- # INSTALLATION:
- # - Copy the 'p4.py' file into your path.
- # - Make sure the 'p4' executable is in your path
- # - Rename 'p4' to '_p4', symlink 'p4.py' to 'p4' (optional)
- #
- #
- # USAGE:
- # p4.py - offline caching
- #
- # Usage: p4.py [COMMAND] FILE...
- #
- # Cached commands:
- # add Open files for adding to the depot
- # edit Open existing files for editing
- # delete Open existing files for removal from the depot
- # revert Revert open files and restore originals to workspace
- #
- # Extended commands:
- # replay Replay all offline cached commands
- #
- #
- # REVISION HISTORY:
- # 1.0.1 (2018-01-12)
- # Use google.com as network indicator
- # 1.0.0 (2018-01-12)
- # initial release
- #
- import os
- import sys
- import subprocess
- import platform
- import socket
- import pickle
- # =============================================================================
- # Helper
- # =============================================================================
- def has_internet():
- try:
- host = socket.gethostbyname("google.com")
- s = socket.create_connection((host, 80), 2)
- return True
- except:
- pass
- return False
- def is_posix():
- return not platform.system() == "Windows"
- def find_root(anchor=".p4ignore"):
- cur_dir = os.getcwd()
- while True:
- file_list = os.listdir(cur_dir)
- parent_dir = os.path.dirname(cur_dir)
- if anchor in file_list:
- return cur_dir
- else:
- if cur_dir == parent_dir: break
- else: cur_dir = parent_dir
- return os.getcwd()
- def to_relative_path(path):
- if type(path).__name__ == "CachedFile":
- return os.path.relpath(path.path)
- return os.path.relpath(path)
- def set_readwrite(path):
- if is_posix():
- p = subprocess.run(["chmod", "+w", path])
- else:
- p = subprocess.run(["attrib", "-r", path])
- if p.returncode != 0:
- raise IOError("Could not set file read/write")
- def set_readonly(path):
- if is_posix():
- p = subprocess.run(["chmod", "-w", path])
- else:
- p = subprocess.run(["attrib", "+r", path])
- if p.returncode != 0:
- raise IOError("Could not set file read-only")
- # =============================================================================
- # P4 forwarding
- # =============================================================================
- def forward_to_p4(argc, argv):
- args = []
- args.extend(["p4"])
- args.extend(argv[1:])
- p = subprocess.run(args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
- sys.stdout.write(p.stdout.decode("utf-8"))
- sys.stdout.flush()
- return p.returncode
- # =============================================================================
- # Offline caching
- # =============================================================================
- class CachedFile:
- def __init__(self, path):
- with open(path, "rb") as f:
- self.content = f.read()
- self.path = os.path.abspath(path)
- set_readwrite(self.path)
- def __eq__(self, rhs):
- if type(rhs).__name__ == "CachedFile":
- return self.path == rhs.path
- return self.path == rhs
- class Cache:
- def __init__(self):
- self.added_files = []
- self.opened_files = []
- self.deleted_files = []
- @staticmethod
- def load_from_file(path):
- with open(path, "rb") as f:
- return pickle.load(f)
- def save_to_file(self, path):
- with open(path, "wb") as f:
- pickle.dump(self, f)
- def is_open(self, path):
- return path in self.opened_files
- def open_file(self, path):
- path = os.path.abspath(path)
- if not path in self.opened_files:
- self.opened_files.append(CachedFile(path))
- print("{} - opened for edit".format(to_relative_path(path)))
- elif path in self.opened_files:
- print("{} - currently opened for edit".format(to_relative_path(path)))
- elif path in self.added_files:
- print("{} - currently opened for add".format(to_relative_path(path)))
- return 0
- def revert_file(self, path):
- path = os.path.abspath(path)
- if path in self.opened_files:
- os.remove(path)
- with open(path, "wb") as f:
- idx = self.opened_files.index(path)
- f.write(self.opened_files[idx].content)
- self.opened_files.remove(path)
- set_readonly(path)
- print("{} - was edit, reverted".format(to_relative_path(path)))
- elif path in self.added_files:
- # Do not actually delete the file, as a safety-measurement.
- self.added_files.remove(path)
- print("{} - was add, abandoned".format(to_relative_path(path)))
- elif path in self.deleted_files:
- with open(path, "wb") as f:
- idx = self.deleted_files.index(path)
- f.write(self.deleted_files[idx].content)
- self.deleted_files.remove(path)
- set_readonly(path)
- print("{} - was delete, reverted".format(to_relative_path(path)))
- return 0
- def add_file(self, path):
- path = os.path.abspath(path)
- if path in self.deleted_files:
- print("{} - can't add (already opened for delete)"
- .format(to_relative_path(path)))
- elif path in self.opened_files:
- print("{} - can't add (already opened for edit)"
- .format(to_relative_path(path)))
- elif not path in self.added_files:
- self.added_files.append(path)
- print("{} - opened for add".format(to_relative_path(path)))
- else:
- print("{} - currently opened for add".format(to_relative_path(path)))
- return 0
- def delete_file(self, path):
- path = os.path.abspath(path)
- if path in self.added_files or path in self.opened_files:
- print("{} - currently opened for delete"
- .format(to_relative_path(path)))
- elif not path in self.deleted_files:
- self.deleted_files.append(CachedFile(path))
- print("{} - opened for delete".format(to_relative_path(path)))
- os.remove(path)
- return 0
- def cache_args(argc, argv, cache_path):
- args = argv[1:]
- # If we have less than two arguments, we can't work our magic.
- if argc < 2:
- return forward_to_p4(argc, argv)
- if os.path.isfile(cache_path):
- cached = Cache.load_from_file(cache_path)
- else:
- cached = Cache()
- def status():
- if len(cached.added_files) == 0 and \
- len(cached.opened_files) == 0 and \
- len(cached.deleted_files) == 0:
- print("no files opened for edit, add or delete")
- else:
- for i in cached.added_files:
- print("{} - add".format(to_relative_path(i)))
- for i in cached.opened_files:
- print("{} - edit".format(to_relative_path(i)))
- for i in cached.deleted_files:
- print("{} - deleted".format(to_relative_path(i)))
- return 0
- returncode = -1
- if args[0] == "add":
- returncode = cached.add_file(args[1])
- elif args[0] == "edit":
- returncode = cached.open_file(args[1])
- elif args[0] == "delete":
- returncode = cached.delete_file(args[1])
- elif args[0] == "revert":
- returncode = cached.revert_file(args[1])
- elif args[0] == "status":
- returncode = status()
- # Save to disk!
- if returncode != -1:
- cached.save_to_file(cache_path)
- return returncode
- return forward_to_p4(argc, argv)
- # =============================================================================
- # Replaying of offline cache
- # =============================================================================
- def replay_offline_cache(cache_path):
- def p4_cmd(cmd, args):
- argv = [ "p4", cmd ]
- if len(args) < 1:
- return 0
- argv.extend(args)
- p = subprocess.run(argv)
- return p.returncode
- cached = Cache.load_from_file(cache_path)
- if p4_cmd("add", cached.added_files) != 0:
- return 1
- if p4_cmd("edit", cached.opened_files) != 0:
- return 1
- if p4_cmd("delete", cached.deleted_files) != 0:
- return 1
- os.remove(cache_path)
- return 0
- # =============================================================================
- # Entry Point
- # =============================================================================
- def main(argc, argv):
- root = find_root()
- # TODO: Add a config file, to allow configuring this?
- # TODO: Add a 'help' command? Intercept 'p4 help' and inject 'p4 help replay'.
- # As well as intercepting 'p4 help replay' and showing the help from above.
- cache_path = os.path.abspath(os.path.join(root, ".p4cache"))
- # Manually replaying the offline cached commands.
- if argc > 1 and argv[1] == "replay":
- return replay_offline_cache(cache_path)
- # Detect whether we have network, forward to p4 if yes, cache otherwise.
- if has_internet():
- if os.path.isfile(cache_path):
- replay_offline_cache(cache_path)
- return forward_to_p4(argc, argv)
- else:
- return cache_args(argc, argv, cache_path)
- if __name__ == "__main__":
- sys.exit(main(len(sys.argv), sys.argv))
Add Comment
Please, Sign In to add comment