import giteapc.gitea as gitea from giteapc.repos import LocalRepo, RemoteRepo from giteapc.util import * from giteapc.config import REPO_FOLDER, PREFIX_FOLDER import fnmatch import shutil import os import re # # Determine full repo names from short versions # def local_match(name): """Find all local repositories with the specified name.""" return [ r for r in LocalRepo.all() if r.name == name ] def remote_match(name): """Find all remote repositories matching the given name.""" return [ r for r in gitea.all_remote_repos() if r.name == name ] def resolve(name, local_only=False, remote_only=False): assert not (local_only and remote_only) # Resolve full names directly if "/" in name: if not remote_only and LocalRepo.exists(name): return LocalRepo(name) if local_only: raise ResolveMissingException(name, local_only, remote_only) r = gitea.repo_get(name) if r is None: raise ResolveMissingException(name, local_only, remote_only) return r # Match local names without owners if not remote_only: r = local_match(name) if len(r) == 0 and local_only: raise ResolveMissingException(name, local_only, remote_only) elif len(r) == 1: return r[0] elif len(r) > 1: raise ResolveAmbiguousException(name, r, "local") # Match remote names without owners if not local_only: r = remote_match(name) if len(r) == 0: raise ResolveMissingException(name, local_only, remote_only) elif len(r) == 1: return r[0] else: raise ResolveAmbiguousException(name, r, "remote") # # Utilities # def print_repo(r, branches=None, tags=None, has_giteapc=True): color = "ARGYBMW"[sum(map(ord,r.owner[:5])) % 7] print(colors()[color] + "{}{_}/{W}{}{_}".format(r.owner, r.name, **colors()), end="") print(" (remote)" if r.remote else " (local)", end="") if r.remote and r.parent: print(" {A}[{}]{_}".format(r.parent.fullname, **colors()), end="") if r.remote and has_giteapc == False: print(" {R}(NOT SUPPORTED){_}".format(**colors()), end="") print("") if r.remote: print(("\n" + r.description).replace("\n", "\n ")[1:]) else: print(" {W}HEAD:{_}".format(**colors()), r.describe(all=True)) print(" {W}Path:{_}".format(**colors()), r.folder, end="") if os.path.islink(r.folder): print(" ->", os.readlink(r.folder)) else: print("") print(" {W}Configs:{_}".format(**colors()), end="") for c in r.configs(): print(" " + c, end="") print("") branches = r.branches() tags = r.tags() if branches: print(" {W}Branches:{_}".format(**colors()), end="") for b in branches: if "local" in b and b["local"] == False: print(" {A}{}{_}".format(b["name"], **colors()), end="") else: print(" " + b["name"], end="") print("") if tags: print(" {W}Tags:{_}".format(**colors()), " ".join(t["name"] for t in reversed(tags))) def pretty_repo(r): color = "ARGYBMW"[sum(map(ord,r.owner[:5])) % 7] return colors()[color] + "{}{_}/{W}{}{_}".format(r.owner,r.name,**colors()) # # Repository specifications # class Spec: # A spec in REPOSITORY[@VERSION][:CONFIGURATION] RE_SPEC = re.compile(r'^([^@:]+)(?:@([^@:]+))?(?:([:])([^@:]*))?') def __init__(self, string): m = re.match(Spec.RE_SPEC, string) if m is None: raise Error(f"wrong format in specification {string}") self.name = m[1] self.version = m[2] self.config = m[4] or (m[3] and "") self.repo = None def resolve(self, local_only=False, remote_only=False): self.repo = resolve(self.name, local_only, remote_only) return self.repo def is_blank(self): return self.version is None and self.config is None def str(self, pretty=False): if self.repo and pretty: name = pretty_repo(self.repo) elif self.repo: name = self.repo.fullname else: name = self.name version = f"@{self.version}" if self.version else "" config = f":{self.config}" if self.config else "" return name + version + config def same_version_config_as(self, other): return self.version == other.version and self.config == other.config def __str__(self): return self.str(pretty=False) def __repr__(self): return self.str(pretty=False) def pretty_str(self): return self.str(pretty=True) # # repo list command # def _list(*args, remote=False): if len(args) > 1: return fatal("repo list: too many arguments") if remote: # Since there aren't many repositories under [giteapc], just list them # and then filter by hand (this avoids some requests and makes search # results more consistent with local repositories) repos = gitea.all_remote_repos() else: repos = LocalRepo.all() # - Ignore case in pattern # - Add '*' at start and end to match substrings only pattern = args[0].lower() if args else "*" if not pattern.startswith("*"): pattern = "*" + pattern if not pattern.endswith("*"): pattern = pattern + "*" # Filter repos = [ r for r in repos if fnmatch.fnmatch(r.fullname.lower(), pattern) or r.remote and fnmatch.fnmatch(r.description.lower(), pattern) ] # Print if repos == []: if args: print(f"no repository matching '{args[0]}'") else: print(f"no repository") return 1 else: for r in repos: print_repo(r) return 0 # # repo fetch command # def fetch(*args, use_ssh=False, use_https=False, force=False, update=False): # Use HTTPS by default if use_ssh and use_https: return fatal("repo fetch: --ssh and --https are mutually exclusive") protocol = "ssh" if use_ssh else "https" # With no arguments, fetch all local repositories if args == (): for r in LocalRepo.all(): msg(f"Fetching {pretty_repo(r)}...") r.fetch() if update: r.pull() return 0 for arg in args: s = arg if isinstance(arg, Spec) else Spec(arg) r = s.resolve() # If this is a local repository, just git fetch if not r.remote: msg(f"Fetching {pretty_repo(r)}...") r.fetch() # If this is a remote repository, clone it else: msg(f"Cloning {pretty_repo(r)}...") has_tag = "giteapc" in gitea.repo_topics(r) if has_tag or force: LocalRepo.clone(r, protocol) if not has_tag and force: warn(f"{r.fullname} doesn't have the [giteapc] tag") if not has_tag and not force: raise Error(f"{r.fullname} doesn't have the [giteapc] tag, "+\ "use -f to force") r = s.resolve() # Checkout requested version, if any if s.version: msg("Checking out {W}{}{_}".format(s.version, **colors())) r.checkout(s.version) if update: r.pull() # # repo show command # def show(*args, remote=False, path=False): if remote and path: raise Error("repo show: -r and -p are exclusive") if not remote: for name in args: r = resolve(name, local_only=True) if path: print(LocalRepo.path(r.fullname)) else: print_repo(r) return 0 repos = [] for name in args: r = resolve(name, remote_only=True) branches = gitea.repo_branches(r) tags = gitea.repo_tags(r) topics = gitea.repo_topics(r) repos.append((r, branches, tags, "giteapc" in topics)) for (r, branches, tags, has_giteapc) in repos: print_repo(r, branches, tags, has_giteapc=has_giteapc) # # repo build command # def build(*args, install=False, skip_configure=False): if len(args) < 1: return fatal("repo build: specify at least one repository") specs = [] for arg in args: s = arg if isinstance(arg, Spec) else Spec(arg) s.resolve(local_only=True) specs.append(s) for s in specs: r = s.repo pretty = pretty_repo(r) config_string = f" for {s.config}" if s.config else "" if s.version: msg("{}: Checking out {W}{}{_}".format(pretty,s.version,**colors())) r.checkout(s.version) # Check that the project has a Makefile if not os.path.exists(r.makefile): raise Error(f"{r.fullname} has no giteapc.make") env = os.environ.copy() if s.config is not None: env["GITEAPC_CONFIG"] = s.config r.set_config(s.config) env["GITEAPC_PREFIX"] = PREFIX_FOLDER if not skip_configure: msg(f"{pretty}: Configuring{config_string}") r.make("configure", env) msg(f"{pretty}: Building") r.make("build", env) if install: msg(f"{pretty}: Installing") r.make("install", env) msg(f"{pretty}: Done! :D") # # repo install command # def search_dependencies(names, fetched, plan, **kwargs): for name in names: s = Spec(name) r = s.resolve() if r.fullname not in fetched: fetch(s, **kwargs) fetched.add(r.fullname) # Re-resolve, as a local repository this time if r.remote: r = s.resolve(local_only=True) # Schedule dependencies before r search_dependencies(r.dependencies(), fetched, plan, **kwargs) plan.append(s) def install(*args, use_https=False, use_ssh=False, update=False, yes=False, dry_run=False): # Update all repositories if args == () and not update: return fatal(f"repo install: need one argument (unless -u is given)") if args == () and update: args = [ r.fullname for r in LocalRepo.all() ] # Fetch every repository and determine its dependencies to form a basic # plan of what repo to build in what order, but without version/config info dep_order = [] search_dependencies(args, set(), dep_order, use_https=use_https, use_ssh=use_ssh, update=update) # Apply configurations on everyone and make sure they are no contradictions # and all configs exist spec_by_repo = { spec.repo.fullname: spec for spec in dep_order } for arg in args: s = Spec(arg) if s.is_blank(): continue r = s.resolve(local_only=True) name = s.repo.fullname if not s.same_version_config_as(spec_by_repo[name]): return fatal(f"repo install: multiple specs for {name}: {s}, " + f"{spec_by_repo[name]}") if s.config not in ["", None] and s.config not in r.configs(): return fatal(f"repo install: no config {s.config} for {name}" + " (configs: " + ", ".join(r.configs()) + ")") spec_by_repo[name] = s # Final plan plan = [ spec_by_repo[s.repo.fullname] for s in dep_order ] # Plan review and confirmation msg("Will install:", ", ".join(s.pretty_str() for s in plan)) if dry_run: return 0 if not yes: msg("Is that okay (Y/n)? ", end="") confirm = input().strip() if confirm not in ["Y", "y", ""]: return 1 # Final build build(*plan, install=True) # # repo uninstall command # def uninstall(*args, keep=False): if len(args) < 1: return fatal("repo uninstall: specify at least one repository") for name in args: r = resolve(name, local_only=True) msg(f"{pretty_repo(r)}: Uninstalling") env = os.environ.copy() env["GITEAPC_PREFIX"] = PREFIX_FOLDER r.make("uninstall", env) if not keep: msg("{}: {R}Removing files{_}".format(pretty_repo(r), **colors())) if os.path.islink(r.folder): os.remove(r.folder) elif os.path.isdir(r.folder): shutil.rmtree(r.folder) else: raise Error(f"cannot handle {r.folder} (not a folder/symlink)") parent = os.path.dirname(r.folder) if not os.listdir(parent): os.rmdir(parent)