| import subprocess
|
| import sys
|
| import os
|
| import traceback
|
| import time
|
|
|
| import git
|
| import json
|
| import yaml
|
| import requests
|
| from tqdm.auto import tqdm
|
| from git.remote import RemoteProgress
|
|
|
|
|
| comfy_path = os.environ.get('COMFYUI_PATH')
|
| git_exe_path = os.environ.get('GIT_EXE_PATH')
|
|
|
| if comfy_path is None:
|
| print("\nWARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.", file=sys.stderr)
|
| comfy_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
|
|
|
| def download_url(url, dest_folder, filename=None):
|
|
|
| if not os.path.exists(dest_folder):
|
| os.makedirs(dest_folder)
|
|
|
|
|
| if filename is None:
|
| filename = os.path.basename(url)
|
|
|
|
|
| dest_path = os.path.join(dest_folder, filename)
|
|
|
|
|
| response = requests.get(url, stream=True)
|
| if response.status_code == 200:
|
| with open(dest_path, 'wb') as file:
|
| for chunk in response.iter_content(chunk_size=1024):
|
| if chunk:
|
| file.write(chunk)
|
| else:
|
| print(f"Failed to download file from {url}")
|
|
|
|
|
| nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
|
| working_directory = os.getcwd()
|
|
|
| if os.path.basename(working_directory) != 'custom_nodes':
|
| print("WARN: This script should be executed in custom_nodes dir")
|
| print(f"DBG: INFO {working_directory}")
|
| print(f"DBG: INFO {sys.argv}")
|
|
|
|
|
|
|
| class GitProgress(RemoteProgress):
|
| def __init__(self):
|
| super().__init__()
|
| self.pbar = tqdm(ascii=True)
|
|
|
| def update(self, op_code, cur_count, max_count=None, message=''):
|
| self.pbar.total = max_count
|
| self.pbar.n = cur_count
|
| self.pbar.pos = 0
|
| self.pbar.refresh()
|
|
|
|
|
| def gitclone(custom_nodes_path, url, target_hash=None, repo_path=None):
|
| repo_name = os.path.splitext(os.path.basename(url))[0]
|
|
|
| if repo_path is None:
|
| repo_path = os.path.join(custom_nodes_path, repo_name)
|
|
|
|
|
| repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
|
|
|
| if target_hash is not None:
|
| print(f"CHECKOUT: {repo_name} [{target_hash}]")
|
| repo.git.checkout(target_hash)
|
|
|
| repo.git.clear_cache()
|
| repo.close()
|
|
|
|
|
| def gitcheck(path, do_fetch=False):
|
| try:
|
|
|
| repo = git.Repo(path)
|
|
|
| if repo.head.is_detached:
|
| print("CUSTOM NODE CHECK: True")
|
| return
|
|
|
| current_branch = repo.active_branch
|
| branch_name = current_branch.name
|
|
|
| remote_name = current_branch.tracking_branch().remote_name
|
| remote = repo.remote(name=remote_name)
|
|
|
| if do_fetch:
|
| remote.fetch()
|
|
|
|
|
| commit_hash = repo.head.commit.hexsha
|
|
|
| if f'{remote_name}/{branch_name}' in repo.refs:
|
| remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
| else:
|
| print("CUSTOM NODE CHECK: True")
|
| return
|
|
|
|
|
| if commit_hash != remote_commit_hash:
|
|
|
| commit_date = repo.head.commit.committed_datetime
|
| remote_commit_date = repo.refs[f'{remote_name}/{branch_name}'].object.committed_datetime
|
|
|
|
|
| if commit_date < remote_commit_date:
|
| print("CUSTOM NODE CHECK: True")
|
| else:
|
| print("CUSTOM NODE CHECK: False")
|
| except Exception as e:
|
| print(e)
|
| print("CUSTOM NODE CHECK: Error")
|
|
|
|
|
| def get_remote_name(repo):
|
| available_remotes = [remote.name for remote in repo.remotes]
|
| if 'origin' in available_remotes:
|
| return 'origin'
|
| elif 'upstream' in available_remotes:
|
| return 'upstream'
|
| elif len(available_remotes) > 0:
|
| return available_remotes[0]
|
|
|
| if not available_remotes:
|
| print(f"[ComfyUI-Manager] No remotes are configured for this repository: {repo.working_dir}")
|
| else:
|
| print(f"[ComfyUI-Manager] Available remotes in '{repo.working_dir}': ")
|
| for remote in available_remotes:
|
| print(f"- {remote}")
|
|
|
| return None
|
|
|
|
|
| def switch_to_default_branch(repo):
|
| remote_name = get_remote_name(repo)
|
|
|
| try:
|
| if remote_name is None:
|
| return False
|
|
|
| default_branch = repo.git.symbolic_ref(f'refs/remotes/{remote_name}/HEAD').replace(f'refs/remotes/{remote_name}/', '')
|
| repo.git.checkout(default_branch)
|
| return True
|
| except:
|
|
|
|
|
| try:
|
| repo.git.checkout(repo.heads.master)
|
| return True
|
| except:
|
| try:
|
| if remote_name is not None:
|
| repo.git.checkout('-b', 'master', f'{remote_name}/master')
|
| return True
|
| except:
|
| try:
|
| repo.git.checkout(repo.heads.main)
|
| return True
|
| except:
|
| try:
|
| if remote_name is not None:
|
| repo.git.checkout('-b', 'main', f'{remote_name}/main')
|
| return True
|
| except:
|
| pass
|
|
|
| print("[ComfyUI Manager] Failed to switch to the default branch")
|
| return False
|
|
|
|
|
| def gitpull(path):
|
|
|
| if not os.path.exists(os.path.join(path, '.git')):
|
| raise ValueError('Not a git repository')
|
|
|
|
|
| repo = git.Repo(path)
|
| if repo.is_dirty():
|
| print(f"STASH: '{path}' is dirty.")
|
| repo.git.stash()
|
|
|
| commit_hash = repo.head.commit.hexsha
|
| try:
|
| if repo.head.is_detached:
|
| switch_to_default_branch(repo)
|
|
|
| current_branch = repo.active_branch
|
| branch_name = current_branch.name
|
|
|
| remote_name = current_branch.tracking_branch().remote_name
|
| remote = repo.remote(name=remote_name)
|
|
|
| if f'{remote_name}/{branch_name}' not in repo.refs:
|
| switch_to_default_branch(repo)
|
| current_branch = repo.active_branch
|
| branch_name = current_branch.name
|
|
|
| remote.fetch()
|
| if f'{remote_name}/{branch_name}' in repo.refs:
|
| remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
| else:
|
| print("CUSTOM NODE PULL: Fail")
|
| return
|
|
|
| if commit_hash == remote_commit_hash:
|
| print("CUSTOM NODE PULL: None")
|
| repo.close()
|
| return
|
|
|
| try:
|
| repo.git.pull('--ff-only')
|
| except git.GitCommandError:
|
| backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
|
| repo.create_head(backup_name)
|
| print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
| repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
| print(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
|
|
| repo.git.submodule('update', '--init', '--recursive')
|
| new_commit_hash = repo.head.commit.hexsha
|
|
|
| if commit_hash != new_commit_hash:
|
| print("CUSTOM NODE PULL: Success")
|
| else:
|
| print("CUSTOM NODE PULL: Fail")
|
| except Exception as e:
|
| print(e)
|
| print("CUSTOM NODE PULL: Fail")
|
|
|
| repo.close()
|
|
|
|
|
| def checkout_comfyui_hash(target_hash):
|
| repo = git.Repo(comfy_path)
|
| commit_hash = repo.head.commit.hexsha
|
|
|
| if commit_hash != target_hash:
|
| try:
|
| print(f"CHECKOUT: ComfyUI [{target_hash}]")
|
| repo.git.checkout(target_hash)
|
| except git.GitCommandError as e:
|
| print(f"Error checking out the ComfyUI: {str(e)}")
|
|
|
|
|
| def checkout_custom_node_hash(git_custom_node_infos):
|
| repo_name_to_url = {}
|
|
|
| for url in git_custom_node_infos.keys():
|
| repo_name = url.split('/')[-1]
|
|
|
| if repo_name.endswith('.git'):
|
| repo_name = repo_name[:-4]
|
|
|
| repo_name_to_url[repo_name] = url
|
|
|
| for path in os.listdir(working_directory):
|
| if path.endswith("ComfyUI-Manager"):
|
| continue
|
|
|
| fullpath = os.path.join(working_directory, path)
|
|
|
| if os.path.isdir(fullpath):
|
| is_disabled = path.endswith(".disabled")
|
|
|
| try:
|
| git_dir = os.path.join(fullpath, '.git')
|
| if not os.path.exists(git_dir):
|
| continue
|
|
|
| need_checkout = False
|
| repo_name = os.path.basename(fullpath)
|
|
|
| if repo_name.endswith('.disabled'):
|
| repo_name = repo_name[:-9]
|
|
|
| if repo_name not in repo_name_to_url:
|
| if not is_disabled:
|
|
|
| print(f"DISABLE: {repo_name}")
|
| new_path = fullpath + ".disabled"
|
| os.rename(fullpath, new_path)
|
| need_checkout = False
|
| else:
|
| item = git_custom_node_infos[repo_name_to_url[repo_name]]
|
| if item['disabled'] and is_disabled:
|
| pass
|
| elif item['disabled'] and not is_disabled:
|
|
|
| print(f"DISABLE: {repo_name}")
|
| new_path = fullpath + ".disabled"
|
| os.rename(fullpath, new_path)
|
|
|
| elif not item['disabled'] and is_disabled:
|
|
|
| print(f"ENABLE: {repo_name}")
|
| new_path = fullpath[:-9]
|
| os.rename(fullpath, new_path)
|
| fullpath = new_path
|
| need_checkout = True
|
| else:
|
| need_checkout = True
|
|
|
| if need_checkout:
|
| repo = git.Repo(fullpath)
|
| commit_hash = repo.head.commit.hexsha
|
|
|
| if commit_hash != item['hash']:
|
| print(f"CHECKOUT: {repo_name} [{item['hash']}]")
|
| repo.git.checkout(item['hash'])
|
|
|
| except Exception:
|
| print(f"Failed to restore snapshots for the custom node '{path}'")
|
|
|
|
|
| for k, v in git_custom_node_infos.items():
|
| if 'ComfyUI-Manager' in k:
|
| continue
|
|
|
| if not v['disabled']:
|
| repo_name = k.split('/')[-1]
|
| if repo_name.endswith('.git'):
|
| repo_name = repo_name[:-4]
|
|
|
| path = os.path.join(working_directory, repo_name)
|
| if not os.path.exists(path):
|
| print(f"CLONE: {path}")
|
| gitclone(working_directory, k, target_hash=v['hash'])
|
|
|
|
|
| def invalidate_custom_node_file(file_custom_node_infos):
|
| global nodelist_path
|
|
|
| enabled_set = set()
|
| for item in file_custom_node_infos:
|
| if not item['disabled']:
|
| enabled_set.add(item['filename'])
|
|
|
| for path in os.listdir(working_directory):
|
| fullpath = os.path.join(working_directory, path)
|
|
|
| if not os.path.isdir(fullpath) and fullpath.endswith('.py'):
|
| if path not in enabled_set:
|
| print(f"DISABLE: {path}")
|
| new_path = fullpath+'.disabled'
|
| os.rename(fullpath, new_path)
|
|
|
| elif not os.path.isdir(fullpath) and fullpath.endswith('.py.disabled'):
|
| path = path[:-9]
|
| if path in enabled_set:
|
| print(f"ENABLE: {path}")
|
| new_path = fullpath[:-9]
|
| os.rename(fullpath, new_path)
|
|
|
|
|
| py_to_url = {}
|
|
|
| with open(nodelist_path, 'r', encoding="UTF-8") as json_file:
|
| info = json.load(json_file)
|
| for item in info['custom_nodes']:
|
| if item['install_type'] == 'copy':
|
| for url in item['files']:
|
| if url.endswith('.py'):
|
| py = url.split('/')[-1]
|
| py_to_url[py] = url
|
|
|
| for item in file_custom_node_infos:
|
| filename = item['filename']
|
| if not item['disabled']:
|
| target_path = os.path.join(working_directory, filename)
|
|
|
| if not os.path.exists(target_path) and filename in py_to_url:
|
| url = py_to_url[filename]
|
| print(f"DOWNLOAD: {filename}")
|
| download_url(url, working_directory)
|
|
|
|
|
| def apply_snapshot(path):
|
| try:
|
| if os.path.exists(path):
|
| if not path.endswith('.json') and not path.endswith('.yaml'):
|
| print(f"Snapshot file not found: `{path}`")
|
| print("APPLY SNAPSHOT: False")
|
| return None
|
|
|
| with open(path, 'r', encoding="UTF-8") as snapshot_file:
|
| if path.endswith('.json'):
|
| info = json.load(snapshot_file)
|
| elif path.endswith('.yaml'):
|
| info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
|
| info = info['custom_nodes']
|
| else:
|
|
|
| print("APPLY SNAPSHOT: False")
|
| return None
|
|
|
| comfyui_hash = info['comfyui']
|
| git_custom_node_infos = info['git_custom_nodes']
|
| file_custom_node_infos = info['file_custom_nodes']
|
|
|
| if comfyui_hash:
|
| checkout_comfyui_hash(comfyui_hash)
|
| checkout_custom_node_hash(git_custom_node_infos)
|
| invalidate_custom_node_file(file_custom_node_infos)
|
|
|
| print("APPLY SNAPSHOT: True")
|
| if 'pips' in info and info['pips']:
|
| return info['pips']
|
| else:
|
| return None
|
|
|
| print(f"Snapshot file not found: `{path}`")
|
| print("APPLY SNAPSHOT: False")
|
|
|
| return None
|
| except Exception as e:
|
| print(e)
|
| traceback.print_exc()
|
| print("APPLY SNAPSHOT: False")
|
|
|
| return None
|
|
|
|
|
| def restore_pip_snapshot(pips, options):
|
| non_url = []
|
| local_url = []
|
| non_local_url = []
|
| for k, v in pips.items():
|
| if v == "":
|
| non_url.append(k)
|
| else:
|
| if v.startswith('file:'):
|
| local_url.append(v)
|
| else:
|
| non_local_url.append(v)
|
|
|
| failed = []
|
| if '--pip-non-url' in options:
|
|
|
| res = 1
|
| try:
|
| res = subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + non_url)
|
| except:
|
| pass
|
|
|
|
|
| if res != 0:
|
| for x in non_url:
|
| res = 1
|
| try:
|
| res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
|
| except:
|
| pass
|
|
|
| if res != 0:
|
| failed.append(x)
|
|
|
| if '--pip-non-local-url' in options:
|
| for x in non_local_url:
|
| res = 1
|
| try:
|
| res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
|
| except:
|
| pass
|
|
|
| if res != 0:
|
| failed.append(x)
|
|
|
| if '--pip-local-url' in options:
|
| for x in local_url:
|
| res = 1
|
| try:
|
| res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
|
| except:
|
| pass
|
|
|
| if res != 0:
|
| failed.append(x)
|
|
|
| print(f"Installation failed for pip packages: {failed}")
|
|
|
|
|
| def setup_environment():
|
| if git_exe_path is not None:
|
| git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe_path)
|
|
|
|
|
| setup_environment()
|
|
|
|
|
| try:
|
| if sys.argv[1] == "--clone":
|
| repo_path = None
|
| if len(sys.argv) > 4:
|
| repo_path = sys.argv[4]
|
|
|
| gitclone(sys.argv[2], sys.argv[3], repo_path=repo_path)
|
| elif sys.argv[1] == "--check":
|
| gitcheck(sys.argv[2], False)
|
| elif sys.argv[1] == "--fetch":
|
| gitcheck(sys.argv[2], True)
|
| elif sys.argv[1] == "--pull":
|
| gitpull(sys.argv[2])
|
| elif sys.argv[1] == "--apply-snapshot":
|
| options = set()
|
| for x in sys.argv:
|
| if x in ['--pip-non-url', '--pip-local-url', '--pip-non-local-url']:
|
| options.add(x)
|
|
|
| pips = apply_snapshot(sys.argv[2])
|
|
|
| if pips and len(options) > 0:
|
| restore_pip_snapshot(pips, options)
|
| sys.exit(0)
|
| except Exception as e:
|
| print(e)
|
| sys.exit(-1)
|
|
|
|
|
|
|