mirror of
https://git.planet-casio.com/Lephenixnoir/GiteaPC.git
synced 2024-12-28 04:23:40 +01:00
most things, minus dependencies, updates, and auto-install
This commit is contained in:
commit
5406afa497
10 changed files with 900 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
__pycache__
|
16
Makefile
Normal file
16
Makefile
Normal file
|
@ -0,0 +1,16 @@
|
|||
PREFIX ?= $(HOME)/.local
|
||||
VERSION = 1.0
|
||||
|
||||
install: $(bin)
|
||||
install -d $(PREFIX)/bin
|
||||
sed -e 's*%PREFIX%*$(PREFIX)*; s*%VERSION%*$(VERSION)*' giteapc.py > $(PREFIX)/bin/giteapc
|
||||
chmod +x $(PREFIX)/bin/giteapc
|
||||
install -d $(PREFIX)/lib/giteapc/giteapc
|
||||
install giteapc/*.py $(PREFIX)/lib/giteapc/giteapc
|
||||
|
||||
uninstall:
|
||||
rm -f $(PREFIX)/bin/giteapc
|
||||
rm -rf $(PREFIX)/lib/giteapc
|
||||
@ echo "note: repositories cloned by GiteaPC have not been removed"
|
||||
|
||||
.PHONY: install uninstall
|
149
giteapc.py
Normal file
149
giteapc.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
#! /usr/bin/env python3
|
||||
|
||||
"""
|
||||
GiteaPC is an automated installer/updated for repositories of the Planète Casio
|
||||
Gitea forge (https://gitea.planet-casio.com/). It is mainly used to set up
|
||||
installs of the add-in development fxSDK.
|
||||
"""
|
||||
|
||||
# Install prefix (inserted at compile-time)
|
||||
PREFIX = "%PREFIX%"
|
||||
# Program version (inserted at compile-time)
|
||||
VERSION = "%VERSION%"
|
||||
|
||||
import sys
|
||||
sys.path.append(PREFIX + "/lib/giteapc")
|
||||
|
||||
import giteapc.repo
|
||||
from giteapc.util import *
|
||||
from giteapc.config import REPO_FOLDER, PREFIX_FOLDER
|
||||
import getopt
|
||||
import sys
|
||||
import os
|
||||
|
||||
# TODO:
|
||||
# * @ or ~ shortcut for remote repositories to avoid the -r option?
|
||||
# * build, install: repository updates
|
||||
# * NetworkError for cURL
|
||||
# * Handle dependencies
|
||||
# * Test update logic
|
||||
|
||||
usage_string = """
|
||||
usage: {R}giteapc{_} [{R}list{_}|{R}fetch{_}|{R}show{_}|{R}build{_}|{R}install{_}|{R}uninstall{_}] [{g}{i}ARGS...{_}]
|
||||
|
||||
GiteaPC is a tool to automatically clone, install and update repositories from
|
||||
the Planète Casio Gitea forge. In the following commands, each {g}{i}REPOSITORY{_}
|
||||
is either a full name like "Lephenixnoir/sh-elf-gcc", or a short name like
|
||||
"sh-elf-gcc" when there is no ambiguity.
|
||||
|
||||
{R}giteapc list{_} [{R}-r{_}] [{g}{i}PATTERN{_}]
|
||||
Lists all repositories on this computer. With -r, lists repositories on the
|
||||
forge. A wildcard pattern can be specified to filter the results.
|
||||
{R}giteapc fetch{_} [{R}--https{_}|{R}--ssh{_}] [{R}-u{_}] [{R}-f{_}] [{g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}]{g}{i}...{_}]
|
||||
Clones or fetches a repository and its dependencies. If no repository is
|
||||
specified, fetches all the local repositories. HTTPS or SSH can be selected
|
||||
when cloning (HTTPS by default). With -u, pulls after fetching.
|
||||
{R}giteapc show{_} [{R}-r{_}] [{R}-p{_}] {g}{i}REPOSITORY...{_}
|
||||
Shows the branches and tags (versions) for the specified local repositories.
|
||||
With -r, show information for remote repositories on the forge.
|
||||
With -p, just print the path of local repositories (useful in scripts).
|
||||
{R}giteapc build{_} [{R}-i{_}] [{R}-u{_}] [{R}--skip-configure{_}] {g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}][{R}:{_}{g}{i}CONFIG{_}]{g}{i}...{_}
|
||||
Configures and builds a local repository. A specific configuration can be
|
||||
requested. With -i, also installs if build is successful. --skip-configure
|
||||
builds without configuring (useful for rebuilds). With -u, pulls the current
|
||||
branch before building (update mode).
|
||||
{R}giteapc install{_} [{R}--https{_}|{R}--ssh{_}] [{R}-u{_}] {g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}][{R}:{_}{g}{i}CONFIG{_}]{g}{i}...{_}
|
||||
Shortcut to clone (or fetch), build, and install a repository.
|
||||
{R}giteapc uninstall{_} [{R}-k{_}] {g}{i}REPOSITORY...{_}
|
||||
Uninstalls the build products of the specified repositories and removes the
|
||||
source files. With -k, keeps the source files.
|
||||
|
||||
{W}Important folders{_}
|
||||
|
||||
{R}$GITEAPC_HOME{_} or {R}$XDG_DATA_HOME/giteapc{_} or {R}$HOME/.local/share/giteapc{_}
|
||||
{W}->{_} {b}{}{_}
|
||||
Storage for GiteaPC repositories.
|
||||
{R}$GITEAPC_PREFIX{_} or {R}$HOME/.local{_}
|
||||
{W}->{_} {b}{}{_}
|
||||
Default install prefix.
|
||||
""".format(REPO_FOLDER, PREFIX_FOLDER, **colors()).strip()
|
||||
|
||||
def usage(exitcode=None):
|
||||
print(usage_string, file=sys.stderr)
|
||||
if exitcode is not None:
|
||||
sys.exit(exitcode)
|
||||
|
||||
commands = {
|
||||
"list": {
|
||||
"function": giteapc.repo.list,
|
||||
"args": "remote:-r,--remote",
|
||||
},
|
||||
"fetch": {
|
||||
"function": giteapc.repo.fetch,
|
||||
"args": "use_ssh:--ssh use_https:--https force:-f,--force "+\
|
||||
"update:-u,--update",
|
||||
},
|
||||
"show": {
|
||||
"function": giteapc.repo.show,
|
||||
"args": "remote:-r,--remote path:-p,--path-only",
|
||||
},
|
||||
"build": {
|
||||
"function": giteapc.repo.build,
|
||||
"args": "install:-i,--install skip_configure:--skip-configure "+\
|
||||
"update:-u,--update",
|
||||
},
|
||||
"install": {
|
||||
"function": giteapc.repo.install,
|
||||
"args": "use_ssh:--ssh use_https:--https update:-u,--update",
|
||||
},
|
||||
"uninstall": {
|
||||
"function": giteapc.repo.uninstall,
|
||||
"args": "keep:-k,--keep",
|
||||
},
|
||||
}
|
||||
|
||||
def main(argv, commands):
|
||||
# Help, version, and invocations without a proper command name
|
||||
if "-h" in argv or "--help" in argv:
|
||||
usage(0)
|
||||
if "-v" in argv or "--version" in argv:
|
||||
print(f"GiteaPC {VERSION}")
|
||||
return 0
|
||||
if argv == [] or argv[0] not in commands:
|
||||
usage(1)
|
||||
|
||||
args = commands[argv[0]].get("args","").split()
|
||||
args = [x.split(":",1) for x in args]
|
||||
args = [(name,forms.split(",")) for name, forms in args]
|
||||
|
||||
single = ""
|
||||
double = []
|
||||
for name, forms in args:
|
||||
for f in forms:
|
||||
if f.startswith("--"):
|
||||
double.append(f[2:])
|
||||
elif f[0] == "-" and len(f) == 2:
|
||||
single += f[1]
|
||||
else:
|
||||
raise Exception("invalid argument format o(x_x)o")
|
||||
|
||||
# Parse arguments
|
||||
try:
|
||||
opts, data = getopt.gnu_getopt(argv[1:], single, double)
|
||||
opts = { name: True if val == "" else val for (name,val) in opts }
|
||||
except getopt.GetoptError as e:
|
||||
return fatal(e)
|
||||
|
||||
options = {}
|
||||
for (o,val) in opts.items():
|
||||
for (name, forms) in args:
|
||||
if o in forms:
|
||||
options[name] = val
|
||||
|
||||
try:
|
||||
return commands[argv[0]]["function"](*data, **options)
|
||||
except Error as e:
|
||||
return fatal(e)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:], commands))
|
0
giteapc/__init__.py
Normal file
0
giteapc/__init__.py
Normal file
9
giteapc/config.py
Normal file
9
giteapc/config.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
import os
|
||||
|
||||
# URL to the Gitea forge supplying the resources
|
||||
GITEA_URL = "https://gitea.planet-casio.com"
|
||||
# Data folder to store repositores
|
||||
XDG_DATA_HOME = os.getenv("XDG_DATA_HOME", os.getenv("HOME")+"/.local/share")
|
||||
REPO_FOLDER = os.getenv("GITEAPC_HOME") or XDG_DATA_HOME + "/giteapc"
|
||||
# Prefix folder to install files to
|
||||
PREFIX_FOLDER = os.getenv("GITEAPC_PREFIX") or os.getenv("HOME") + "/.local"
|
61
giteapc/gitea.py
Normal file
61
giteapc/gitea.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from giteapc.util import *
|
||||
import giteapc.repos
|
||||
import giteapc.config
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Provide a mechanism to submit HTTP GET requests to the forge. Try to use the
|
||||
# requests module if it's available, otherwise default to cURL. If none is
|
||||
# available, disable network features but keep going.
|
||||
_http_get = None
|
||||
|
||||
try:
|
||||
import requests
|
||||
_http_get = requests_get
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if _http_get is None and has_curl():
|
||||
_http_get = curl_get
|
||||
|
||||
if _http_get is None:
|
||||
warn("found neither requests nor curl, network will be disabled")
|
||||
|
||||
# Send a GET request to the specified API endpoint
|
||||
def _get(url, params=None):
|
||||
if _http_get is None:
|
||||
raise Error("cannot access forge (network is disabled)")
|
||||
return _http_get(giteapc.config.GITEA_URL + "/api/v1" + url, params)
|
||||
|
||||
# Search for repositories
|
||||
def repo_search(keyword, **kwargs):
|
||||
r = _get("/repos/search", { "q": keyword, **kwargs })
|
||||
results = json.loads(r)['data']
|
||||
results = [ giteapc.repos.RemoteRepo(r) for r in results ]
|
||||
return sorted(results, key=lambda r: r.fullname)
|
||||
|
||||
# List all remote repositories (with topic "giteapc")
|
||||
def all_remote_repos():
|
||||
return repo_search("giteapc", topic=True)
|
||||
|
||||
# Repository info
|
||||
def repo_get(fullname):
|
||||
try:
|
||||
r = _get(f"/repos/{fullname}")
|
||||
return giteapc.repos.RemoteRepo(json.loads(r))
|
||||
except NetworkError as e:
|
||||
if e.status == 404:
|
||||
return None
|
||||
else:
|
||||
raise e
|
||||
|
||||
def repo_branches(r):
|
||||
r = _get(r.url + "/branches")
|
||||
return json.loads(r)
|
||||
def repo_tags(r):
|
||||
r = _get(r.url + "/tags")
|
||||
return json.loads(r)
|
||||
def repo_topics(r):
|
||||
r = _get(r.url + "/topics")
|
||||
return json.loads(r)["topics"]
|
316
giteapc/repo.py
Normal file
316
giteapc/repo.py
Normal file
|
@ -0,0 +1,316 @@
|
|||
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}Path:{_}".format(**colors()), r.folder, end="")
|
||||
if os.path.islink(r.folder):
|
||||
print(" ->", os.readlink(r.folder))
|
||||
else:
|
||||
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())
|
||||
|
||||
def split_config(name):
|
||||
"""Splits REPOSITORY[@VERSION][:CONFIGURATION] into components."""
|
||||
RE_CONFIG = re.compile(r'^([^@:]+)(?:@([^@:]+))?(?:[:]([^@:]+))?')
|
||||
m = re.match(RE_CONFIG, name)
|
||||
if m is None:
|
||||
return None
|
||||
|
||||
repo, version, config = m[1], m[2] or "", m[3] or ""
|
||||
return repo, version, config
|
||||
|
||||
#
|
||||
# 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 spec in args:
|
||||
name, version, config = split_config(spec)
|
||||
r = resolve(name)
|
||||
|
||||
# If this is a local repository, just git fetch
|
||||
if not r.remote:
|
||||
if version:
|
||||
msg("Checking out {W}{}{_}".format(version, **colors()))
|
||||
r.checkout(version)
|
||||
|
||||
msg(f"Fetching {pretty_repo(r)}...")
|
||||
r.fetch()
|
||||
if update:
|
||||
r.pull()
|
||||
continue
|
||||
|
||||
msg(f"Cloning {pretty_repo(r)}...")
|
||||
|
||||
# For remote repositories, make sure the repository supports GiteaPC
|
||||
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:
|
||||
fatal(f"{r.fullname} doesn't have the [giteapc] tag, use -f to force")
|
||||
|
||||
#
|
||||
# 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, update=False):
|
||||
if len(args) < 1:
|
||||
return fatal("repo build: specify at least one repository")
|
||||
|
||||
specs = []
|
||||
for spec in args:
|
||||
repo, version, config = split_config(spec)
|
||||
repo = resolve(repo, local_only=True)
|
||||
specs.append((repo, version, config))
|
||||
|
||||
msg("Will build:", ", ".join(pretty_repo(spec[0]) for spec in specs))
|
||||
|
||||
for (r, version, config) in specs:
|
||||
pretty = pretty_repo(r)
|
||||
config_string = f" for {config}" if config else ""
|
||||
|
||||
if version != "":
|
||||
msg("{}: Checking out {W}{}{_}".format(pretty, version, **colors()))
|
||||
r.checkout(version)
|
||||
if update:
|
||||
r.pull()
|
||||
|
||||
# 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 config:
|
||||
env["GITEAPC_CONFIG"] = 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 install(*args, use_https=False, use_ssh=False, update=False):
|
||||
if args == ():
|
||||
return 0
|
||||
|
||||
# First download every repository, and only then build
|
||||
fetch(*args, use_https=use_https, use_ssh=use_ssh)
|
||||
build(*args, install=True, update=update)
|
||||
|
||||
#
|
||||
# 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.isdir(r.folder):
|
||||
shutil.rmtree(r.folder)
|
||||
elif os.path.islink(r.folder):
|
||||
os.remove(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)
|
136
giteapc/repos.py
Normal file
136
giteapc/repos.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
from giteapc.config import REPO_FOLDER
|
||||
from giteapc.util import *
|
||||
import subprocess
|
||||
from subprocess import PIPE
|
||||
import os.path
|
||||
import shutil
|
||||
import glob
|
||||
|
||||
class RemoteRepo:
|
||||
# Create a remote repo from the JSON object returned by Gitea
|
||||
def __init__(self, j):
|
||||
self.j = j
|
||||
self.remote = True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.j["name"]
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return self.j["owner"]["username"]
|
||||
|
||||
@property
|
||||
def fullname(self):
|
||||
return self.j["full_name"]
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.j["description"]
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
p = self.j["parent"]
|
||||
return RemoteRepo(p) if p is not None else None
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f"/repos/{self.owner}/{self.name}"
|
||||
|
||||
def clone_url(self, protocol):
|
||||
if protocol == "ssh":
|
||||
return self.j["ssh_url"]
|
||||
else:
|
||||
return self.j["clone_url"]
|
||||
|
||||
|
||||
class LocalRepo:
|
||||
# Create a remote repo from the full name or path
|
||||
def __init__(self, fullname):
|
||||
if fullname.startswith(REPO_FOLDER + "/"):
|
||||
fullname = fullname[len(REPO_FOLDER)+1:]
|
||||
|
||||
assert fullname.count("/") == 1
|
||||
|
||||
self.fullname = fullname
|
||||
self.owner, self.name = fullname.split("/")
|
||||
self.folder = REPO_FOLDER + "/" + fullname
|
||||
self.remote = False
|
||||
self.makefile = self.folder + "/giteapc.make"
|
||||
|
||||
@staticmethod
|
||||
def path(fullname):
|
||||
return REPO_FOLDER + "/" + fullname
|
||||
|
||||
@staticmethod
|
||||
def exists(fullname):
|
||||
return os.path.exists(LocalRepo.path(fullname))
|
||||
|
||||
@staticmethod
|
||||
def all():
|
||||
return [ LocalRepo(path) for path in glob.glob(REPO_FOLDER + f"/*/*") ]
|
||||
|
||||
@staticmethod
|
||||
def clone(r, method="https"):
|
||||
src = r.clone_url(method)
|
||||
dst = LocalRepo.path(r.fullname)
|
||||
mkdir_p(dst)
|
||||
cmd = ["git", "clone", src, dst]
|
||||
|
||||
try:
|
||||
run(cmd, stdout=PIPE, stderr=PIPE)
|
||||
return LocalRepo(r.fullname)
|
||||
except ProcessError as e:
|
||||
# On error, delete the failed clone
|
||||
shutil.rmtree(dst)
|
||||
raise e
|
||||
|
||||
# Git commands
|
||||
|
||||
def _git(self, command, *args, **kwargs):
|
||||
return run(["git", "-C", self.folder] + command,
|
||||
*args, **kwargs)
|
||||
|
||||
def is_on_branch(self):
|
||||
try:
|
||||
self._git(["symbolic-ref", "-q", "HEAD"])
|
||||
return True
|
||||
except ProcessError as e:
|
||||
if e.returncode == 1:
|
||||
return False
|
||||
raise e
|
||||
|
||||
def fetch(self):
|
||||
self._git(["fetch"])
|
||||
|
||||
def pull(self):
|
||||
if self.is_on_branch():
|
||||
self._git(["pull"])
|
||||
|
||||
def checkout(self, version):
|
||||
self._git(["checkout", version], stdout=PIPE, stderr=PIPE)
|
||||
|
||||
def branches(self):
|
||||
proc = self._git(["branch"], stdout=subprocess.PIPE)
|
||||
local = proc.stdout.decode("utf-8").split("\n")
|
||||
local = [ b[2:] for b in local if b ]
|
||||
|
||||
proc = self._git(["branch", "-r"], stdout=subprocess.PIPE)
|
||||
remote = proc.stdout.decode("utf-8").split("\n")
|
||||
remote = [ b[9:] for b in remote
|
||||
if b and b.startswith(" origin/") and "/HEAD " not in b ]
|
||||
remote = [ b for b in remote if b not in local ]
|
||||
|
||||
return [ {"name": b, "local": True } for b in local ] + \
|
||||
[ {"name": b, "local": False} for b in remote ]
|
||||
|
||||
def tags(self):
|
||||
proc = self._git(["tag", "--list"], stdout=subprocess.PIPE)
|
||||
tags = proc.stdout.decode("utf-8").split("\n")
|
||||
return [ {"name": t} for t in tags if t ]
|
||||
|
||||
# Make commands
|
||||
|
||||
def make(self, target, env=None):
|
||||
with ChangeDirectory(self.folder):
|
||||
return run(["make", "-f", "giteapc.make", target], env=env)
|
156
giteapc/util.py
Normal file
156
giteapc/util.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
import sys
|
||||
import subprocess
|
||||
import os.path
|
||||
import shlex
|
||||
|
||||
# Error management; the base Error class is provided for errors to be caught at
|
||||
# top-level and displayed as a GiteaPC error message.
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
class NetworkError(Error):
|
||||
def __init__(self, status):
|
||||
self.status = status
|
||||
|
||||
class ProcessError(Error):
|
||||
def __init__(self, process):
|
||||
self.process = process
|
||||
self.returncode = process.returncode
|
||||
|
||||
def __str__(self):
|
||||
p = self.process
|
||||
e = f"error {p.returncode} in command: "
|
||||
c = " ".join(shlex.quote(arg) for arg in p.args)
|
||||
e += "{W}{}{_}\n".format(c, **colors())
|
||||
|
||||
out = p.stdout.decode('utf-8').strip() if p.stdout else ""
|
||||
err = p.stderr.decode('utf-8').strip() if p.stderr else ""
|
||||
|
||||
# Show standard output and standard error (omit names if only one has
|
||||
# content to show)
|
||||
if len(out) > 0 and len(err) > 0:
|
||||
e += "{W}Standard output:{_}\n".format(**colors())
|
||||
if len(out) > 0:
|
||||
e += out + "\n"
|
||||
if len(out) > 0 and len(err) > 0:
|
||||
e += "{W}Standard error:{_}\n".format(**colors())
|
||||
if len(err) > 0:
|
||||
e += err + "\n"
|
||||
|
||||
return e.strip()
|
||||
|
||||
class ResolveMissingException(Error):
|
||||
def __init__(self, name, local_only, remote_only):
|
||||
self.name = name
|
||||
self.local_only = local_only
|
||||
self.remote_only = remote_only
|
||||
|
||||
def __str__(self):
|
||||
if self.local_only:
|
||||
spec = "local repository"
|
||||
elif self.remote_only:
|
||||
spec = "remote repository"
|
||||
else:
|
||||
spec = "local or remote repository"
|
||||
|
||||
return f"no such {spec}: '{self.name}'"
|
||||
|
||||
class ResolveAmbiguousException(Error):
|
||||
def __init__(self, name, matches, kind):
|
||||
self.name = name
|
||||
self.matches = [r.fullname for r in matches]
|
||||
self.kind = kind
|
||||
|
||||
def __str__(self):
|
||||
return f"multiple {self.kind} repositories match '{self.name}': " + \
|
||||
", ".join(self.matches)
|
||||
|
||||
def warn(*args):
|
||||
print("{Y}warning:{_}".format(**colors()), *args, file=sys.stderr)
|
||||
def fatal(*args):
|
||||
print("{R}error:{_}".format(**colors()), *args, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Color output
|
||||
|
||||
def colors():
|
||||
colors = {
|
||||
# Gray, Red, Green, Yello, Blue, Magenta, Cyan, White
|
||||
"A": "30;1", "R": "31;1", "G": "32;1", "Y": "33;1",
|
||||
"B": "34;1", "M": "35;1", "C": "36;1", "W": "37;1",
|
||||
# Same but without bold
|
||||
"a": "30", "r": "31", "g": "32", "y": "33",
|
||||
"b": "34", "m": "35", "c": "36", "w": "37",
|
||||
# Italic
|
||||
"i": "3",
|
||||
# Clear formatting
|
||||
"_": "0",
|
||||
}
|
||||
# Disable colors if stdout is not a TTY
|
||||
if sys.stdout.isatty():
|
||||
return { name: f"\x1b[{code}m" for name, code in colors.items() }
|
||||
else:
|
||||
return { name: "" for name in colors }
|
||||
|
||||
def msg(*args, **kwargs):
|
||||
print("{c}<giteapc>{_}".format(**colors()), *args, **kwargs)
|
||||
|
||||
# Change directory and guarantee changing back even if an exception occurs
|
||||
|
||||
class ChangeDirectory:
|
||||
def __init__(self, destination):
|
||||
self.destination = destination
|
||||
|
||||
def __enter__(self):
|
||||
self.source = os.getcwd()
|
||||
os.chdir(self.destination)
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
os.chdir(self.source)
|
||||
|
||||
# Tool detection
|
||||
|
||||
def has_curl():
|
||||
proc = subprocess.run(["curl", "--version"], stdout=subprocess.DEVNULL)
|
||||
return proc.returncode == 0
|
||||
|
||||
def has_git():
|
||||
proc = subprocess.run(["git", "--version"], stdout=subprocess.DEVNULL)
|
||||
return proc.returncode == 0
|
||||
|
||||
# HTTP requests
|
||||
|
||||
def requests_get(url, params):
|
||||
# Don't import requests until the gitea module confirms it's available
|
||||
import requests
|
||||
r = requests.get(url, params)
|
||||
if r.status_code != 200:
|
||||
raise NetworkError(r.status_code)
|
||||
return r.text
|
||||
|
||||
def curl_get(url, params):
|
||||
if params:
|
||||
url = url + "?" + "&".join(f"{n}={v}" for (n,v) in params.items())
|
||||
proc = subprocess.run(["curl", "-s", url], stdout=subprocess.PIPE)
|
||||
assert proc.returncode == 0
|
||||
return proc.stdout.decode("utf-8")
|
||||
|
||||
# Create path to folder
|
||||
|
||||
def mkdir_p(folder):
|
||||
try:
|
||||
os.mkdir(folder)
|
||||
except FileExistsError:
|
||||
return
|
||||
except FileNotFoundError:
|
||||
mkdir_p(os.path.dirname(folder))
|
||||
os.mkdir(folder)
|
||||
|
||||
# Run process and throw exception on error
|
||||
|
||||
def run(process, *args, **kwargs):
|
||||
proc = subprocess.run(process, *args, **kwargs)
|
||||
if proc.returncode != 0:
|
||||
raise ProcessError(proc)
|
||||
return proc
|
56
install.sh
Executable file
56
install.sh
Executable file
|
@ -0,0 +1,56 @@
|
|||
#! /usr/bin/env bash
|
||||
|
||||
TAG=$(printf "\x1b[36m<giteapc>\x1b[0m")
|
||||
PREFIX=${GITEAPC_PREFIX:-$HOME/.local}
|
||||
URL="https://gitea.planet-casio.com/Lephenixnoir/giteapc/archive/master.tar.gz"
|
||||
|
||||
# Download the source code
|
||||
|
||||
cd $(mktemp)
|
||||
curl $URL -o giteapc-master.tar.gz
|
||||
tar -xzf giteapc-master.tar.gz && cd giteapc
|
||||
|
||||
# Install the program itself (not to $PREFIX, which is for programs installed
|
||||
# by GiteaPC)
|
||||
|
||||
make install
|
||||
|
||||
# Check whether the bin folder is already in the PATH
|
||||
|
||||
if [[ ":$PATH:" =~ ":$PREFIX/bin:" ]]; then
|
||||
echo "$TAG $PREFIX/bin is already in your PATH, we're good to go!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try to find a suitable startup file to extend the PATH in
|
||||
|
||||
default="$HOME/.profile"
|
||||
candidates=".bashrc .zshrc .bash_profile .profile .zprofile"
|
||||
|
||||
for c in $candidates; do
|
||||
[[ -f "$HOME/$c" ]] && default="$HOME/$c"
|
||||
done
|
||||
|
||||
# Suggest to add the path to binaries to the PATH at startup
|
||||
|
||||
cat <<EOF
|
||||
$TAG In order to use programs installed by GiteaPC, you will need to add their
|
||||
$TAG install folder to your PATH. This can be done automatically when you log
|
||||
$TAG in by adding the following command to your startup file:
|
||||
$TAG
|
||||
$TAG export PATH="\$PATH:$PREFIX/bin"
|
||||
$TAG
|
||||
$TAG -> Press Enter to add this command to $default, or
|
||||
$TAG -> Type another file name to add this command to, or
|
||||
$TAG -> Type "-" to skip setting the PATH entirely.
|
||||
EOF
|
||||
|
||||
read -p "> " startup_file
|
||||
[[ -z "$startup_file" ]] && startup_file=$default
|
||||
|
||||
if [[ "$startup_file" == "-" ]]; then
|
||||
echo "$TAG Skipped setting the PATH."
|
||||
else
|
||||
echo "export PATH=\"\$PATH:$PREFIX/bin\"" >> $startup_file
|
||||
echo "$TAG Set the PATH in $startup_file, this will take effect next login."
|
||||
fi
|
Loading…
Reference in a new issue