Last active
January 9, 2022 00:09
-
-
Save Leandros/bf099f9e880dcbbe800a84b090a2fe58 to your computer and use it in GitHub Desktop.
p4.py - p4 offline caching
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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)) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
set -x | |
# Setup | |
TEMPDIR=$(mktemp -d) | |
cd $TEMPDIR | |
# Marks the P4 root of a depot. | |
touch .p4ignore | |
# Cleanup on exit. | |
function onExit { | |
cd $DIR | |
rm -rf "$TEMPDIR" | |
} | |
trap onExit EXIT | |
# ============================================================================= | |
# Tests | |
# ============================================================================= | |
# !! ATTENTION !! | |
# The following tests assume that there is never an internet connection! | |
# Or that the 'p4.py' script has been modified to 'return False` | |
# from 'has_internet()' in all cases | |
# 1.1 'p4 add' | |
echo "123test123" > add.txt | |
$DIR/p4.py add add.txt | grep -E "add.txt \- opened for add\>" | |
$DIR/p4.py status | grep -E "add.txt \- add\>" | |
$DIR/p4.py revert add.txt | grep -E "add.txt \- was add, abandoned\>" | |
cat add.txt | grep -E "\<123test123\>" | |
$DIR/p4.py status | grep -E "\<no files opened for edit, add or delete\>" | |
# 1.2: 'p4 edit' | |
$DIR/p4.py edit add.txt | grep -E "add.txt \- opened for edit\>" | |
$DIR/p4.py status | grep -E "add.txt \- edit\>" | |
echo "" > add.txt | |
$DIR/p4.py revert add.txt | grep -E "add.txt \- was edit, reverted\>" | |
cat add.txt | grep -E "\<123test123\>" | |
$DIR/p4.py status | grep -E "\<no files opened for edit, add or delete\>" | |
# 1.3: 'p4 delete' | |
$DIR/p4.py delete add.txt | grep -E "add.txt \- opened for delete\>" | |
if [ -f "add.txt" ]; then exit 1; fi | |
$DIR/p4.py status | grep -E "add.txt \- deleted\>" | |
$DIR/p4.py revert add.txt | grep -E "add.txt \- was delete, reverted\>" | |
cat add.txt | grep -E "\<123test123\>" | |
$DIR/p4.py status | grep -E "\<no files opened for edit, add or delete\>" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment