The erplibre root repo was appended last and hard to find among 141 entries. Sort the list so repos are easy to locate by name. Generated by Claude Code 2.1.72 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
874 lines
23 KiB
Python
Executable file
874 lines
23 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# © 2025-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import argparse
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
new_path = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
)
|
|
sys.path.append(new_path)
|
|
|
|
from script.execute.execute import Execute
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_ERPLIBRE_PATH = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
)
|
|
DEFAULT_GIT_PATH = os.path.join(os.path.expanduser("~"), ".git-server")
|
|
PRODUCTION_GIT_PATH = "/srv/git"
|
|
DEFAULT_MANIFEST = ".repo/local_manifests/erplibre_manifest.xml"
|
|
DEFAULT_REMOTE_NAME = "local"
|
|
DEFAULT_PORT = 9418
|
|
DEFAULT_JOBS = 8
|
|
ERPLIBRE_REPO_NAME = "erplibre/erplibre"
|
|
ERPLIBRE_REPO_URL = "https://github.com/erplibre"
|
|
|
|
|
|
async def _run_git(*args, cwd=None, timeout=None):
|
|
"""Run a git command asynchronously.
|
|
|
|
Returns (stdout, stderr, returncode).
|
|
Raises asyncio.TimeoutError on timeout.
|
|
"""
|
|
process = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=cwd,
|
|
)
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(
|
|
process.communicate(), timeout=timeout
|
|
)
|
|
return (
|
|
stdout.decode(),
|
|
stderr.decode(),
|
|
process.returncode,
|
|
)
|
|
except asyncio.TimeoutError:
|
|
process.kill()
|
|
await process.communicate()
|
|
raise
|
|
|
|
|
|
def get_config():
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description="""\
|
|
Deploy a local git server using git daemon and configure
|
|
all repos from the ERPLibre manifest.
|
|
|
|
Actions (in order):
|
|
1. init - Create bare repos in the git server path
|
|
2. remote - Add 'local' remote to each repo
|
|
3. push - Push all branches to the local remote
|
|
4. serve - Start git daemon
|
|
|
|
By default, all actions are executed sequentially.
|
|
The default path is ~/.git-server (user-level, no root needed).
|
|
Use --production-ready for /srv/git (requires root).
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--path",
|
|
default=None,
|
|
help=(
|
|
"Path for git server bare repos" f" (default: {DEFAULT_GIT_PATH})"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--production-ready",
|
|
action="store_true",
|
|
help=(
|
|
f"Use {PRODUCTION_GIT_PATH} as git server path"
|
|
" (requires root privileges)"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--manifest",
|
|
default=DEFAULT_MANIFEST,
|
|
help="Manifest XML file" f" (default: {DEFAULT_MANIFEST})",
|
|
)
|
|
parser.add_argument(
|
|
"--remote-name",
|
|
default=DEFAULT_REMOTE_NAME,
|
|
help="Remote name to add" f" (default: {DEFAULT_REMOTE_NAME})",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=DEFAULT_PORT,
|
|
help=f"Git daemon port (default: {DEFAULT_PORT})",
|
|
)
|
|
parser.add_argument(
|
|
"--action",
|
|
choices=["init", "remote", "push", "serve", "all"],
|
|
default="all",
|
|
help="Action to perform (default: all)",
|
|
)
|
|
parser.add_argument(
|
|
"--erplibre-root",
|
|
default=None,
|
|
help="ERPLibre root directory (default: auto-detect)",
|
|
)
|
|
parser.add_argument(
|
|
"--push-all-branches",
|
|
action="store_true",
|
|
help="Push all local branches, not just the current one",
|
|
)
|
|
parser.add_argument(
|
|
"--remote-url-type",
|
|
choices=["file", "daemon"],
|
|
default="file",
|
|
help=(
|
|
"Remote URL type: 'file' uses local path"
|
|
" (push works without daemon), 'daemon'"
|
|
" uses git:// protocol (default: file)"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--unshallow",
|
|
action="store_true",
|
|
help=(
|
|
"Fetch full history for shallow repos before"
|
|
" push (slow for large repos like odoo)."
|
|
" Default: push shallow for performance"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"-j",
|
|
"--jobs",
|
|
type=int,
|
|
default=DEFAULT_JOBS,
|
|
help=(
|
|
"Parallel jobs for init/remote/push"
|
|
f" (default: {DEFAULT_JOBS})"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"-v",
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Enable verbose output",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Resolve git server path
|
|
if args.path and args.production_ready:
|
|
parser.error("--path and --production-ready are mutually exclusive")
|
|
if args.production_ready:
|
|
args.path = PRODUCTION_GIT_PATH
|
|
elif args.path is None:
|
|
args.path = DEFAULT_GIT_PATH
|
|
|
|
return args
|
|
|
|
|
|
def check_path_permissions(path):
|
|
"""Check if we have write permissions on the target
|
|
path. Try the path itself first, then its closest
|
|
existing parent directory."""
|
|
check = path
|
|
while check and not os.path.exists(check):
|
|
check = os.path.dirname(check)
|
|
if not check:
|
|
check = "/"
|
|
if os.access(check, os.W_OK):
|
|
return True
|
|
return False
|
|
|
|
|
|
def require_permissions_or_exit(path):
|
|
"""Exit with instructions if we cannot write to path."""
|
|
print(f"Error: No write permission on {path}")
|
|
print(
|
|
"Please run this script with appropriate"
|
|
" privileges:"
|
|
f"\n sudo {sys.executable}"
|
|
f" {' '.join(sys.argv)}"
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
def parse_manifest(manifest_path):
|
|
"""Parse the manifest XML and return list of projects."""
|
|
tree = ET.parse(manifest_path)
|
|
root = tree.getroot()
|
|
|
|
remotes = {}
|
|
for remote in root.findall("remote"):
|
|
remotes[remote.get("name")] = remote.get("fetch")
|
|
|
|
projects = []
|
|
for project in root.findall("project"):
|
|
name = project.get("name")
|
|
path = project.get("path")
|
|
remote = project.get("remote")
|
|
revision = project.get("revision")
|
|
projects.append(
|
|
{
|
|
"name": name,
|
|
"path": path,
|
|
"remote": remote,
|
|
"revision": revision,
|
|
"fetch_url": remotes.get(remote, ""),
|
|
}
|
|
)
|
|
|
|
return projects
|
|
|
|
|
|
def get_erplibre_root_project(erplibre_root):
|
|
"""Build a project entry for the ERPLibre root repo.
|
|
|
|
The root repo is not in the manifest (it is managed
|
|
by git directly, not Google Repo) but needs to be
|
|
included in the local git server.
|
|
"""
|
|
result = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
erplibre_root,
|
|
"rev-parse",
|
|
"--abbrev-ref",
|
|
"HEAD",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
revision = (
|
|
result.stdout.strip()
|
|
if result.returncode == 0
|
|
else "master"
|
|
)
|
|
|
|
return {
|
|
"name": ERPLIBRE_REPO_NAME,
|
|
"path": ".",
|
|
"remote": "origin",
|
|
"revision": revision,
|
|
"fetch_url": ERPLIBRE_REPO_URL,
|
|
}
|
|
|
|
|
|
# --- Async workers for init ---
|
|
|
|
|
|
async def _init_single_bare_repo(git_path, project, semaphore):
|
|
"""Create a single bare repo (async worker)."""
|
|
async with semaphore:
|
|
repo_name = project["name"]
|
|
if not repo_name.endswith(".git"):
|
|
repo_name += ".git"
|
|
bare_path = os.path.join(git_path, repo_name)
|
|
|
|
if os.path.exists(bare_path):
|
|
_logger.info(f" Exists: {bare_path}")
|
|
return "skipped"
|
|
|
|
_logger.info(f" Creating: {bare_path}")
|
|
_, err, rc = await _run_git(
|
|
"git", "init", "--bare", bare_path
|
|
)
|
|
if rc != 0:
|
|
_logger.warning(
|
|
f" Init failed: {bare_path}: {err.strip()}"
|
|
)
|
|
return "error"
|
|
|
|
# Enable git daemon export
|
|
export_file = os.path.join(
|
|
bare_path, "git-daemon-export-ok"
|
|
)
|
|
open(export_file, "w").close()
|
|
|
|
# Allow pushing from shallow clones
|
|
await _run_git(
|
|
"git",
|
|
"-C",
|
|
bare_path,
|
|
"config",
|
|
"receive.shallowUpdate",
|
|
"true",
|
|
)
|
|
|
|
return "created"
|
|
|
|
|
|
async def init_bare_repos(git_path, projects, jobs):
|
|
"""Create bare repos for all projects (async)."""
|
|
os.makedirs(git_path, exist_ok=True)
|
|
semaphore = asyncio.Semaphore(jobs)
|
|
|
|
results = await asyncio.gather(
|
|
*[
|
|
_init_single_bare_repo(git_path, p, semaphore)
|
|
for p in projects
|
|
]
|
|
)
|
|
|
|
created = results.count("created")
|
|
skipped = results.count("skipped")
|
|
errors = results.count("error")
|
|
print(
|
|
f"Bare repos: {created} created,"
|
|
f" {skipped} skipped (already exist),"
|
|
f" {errors} errors"
|
|
)
|
|
|
|
|
|
# --- Async workers for remote ---
|
|
|
|
|
|
def build_remote_url(git_path, repo_name, port, remote_url_type):
|
|
"""Build the remote URL based on the type."""
|
|
if remote_url_type == "daemon":
|
|
if port != DEFAULT_PORT:
|
|
return f"git://localhost:{port}/{repo_name}"
|
|
return f"git://localhost/{repo_name}"
|
|
# Default: file path
|
|
return os.path.join(git_path, repo_name)
|
|
|
|
|
|
async def _add_single_remote(
|
|
erplibre_root,
|
|
git_path,
|
|
project,
|
|
remote_name,
|
|
port,
|
|
remote_url_type,
|
|
semaphore,
|
|
):
|
|
"""Add or update remote for a single repo (async)."""
|
|
async with semaphore:
|
|
repo_path = os.path.join(
|
|
erplibre_root, project["path"]
|
|
)
|
|
if not os.path.isdir(repo_path):
|
|
_logger.warning(f" Not found: {repo_path}")
|
|
return "error"
|
|
|
|
repo_name = project["name"]
|
|
if not repo_name.endswith(".git"):
|
|
repo_name += ".git"
|
|
|
|
remote_url = build_remote_url(
|
|
git_path, repo_name, port, remote_url_type
|
|
)
|
|
|
|
# Check if remote already exists
|
|
stdout, _, _ = await _run_git(
|
|
"git", "-C", repo_path, "remote"
|
|
)
|
|
existing_remotes = stdout.strip().split("\n")
|
|
|
|
if remote_name in existing_remotes:
|
|
_, err, rc = await _run_git(
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"remote",
|
|
"set-url",
|
|
remote_name,
|
|
remote_url,
|
|
)
|
|
if rc != 0:
|
|
_logger.warning(
|
|
f" set-url failed for"
|
|
f" {project['path']}: {err.strip()}"
|
|
)
|
|
return "error"
|
|
_logger.info(f" Updated: {project['path']}")
|
|
return "updated"
|
|
else:
|
|
_, err, rc = await _run_git(
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"remote",
|
|
"add",
|
|
remote_name,
|
|
remote_url,
|
|
)
|
|
if rc != 0:
|
|
_logger.warning(
|
|
f" add failed for"
|
|
f" {project['path']}: {err.strip()}"
|
|
)
|
|
return "error"
|
|
_logger.info(f" Added: {project['path']}")
|
|
return "added"
|
|
|
|
|
|
async def add_remotes(
|
|
erplibre_root,
|
|
git_path,
|
|
projects,
|
|
remote_name,
|
|
port,
|
|
remote_url_type,
|
|
jobs,
|
|
):
|
|
"""Add local remote to each repo (async).
|
|
|
|
remote_url_type='file': uses local path, push works
|
|
without daemon running.
|
|
remote_url_type='daemon': uses git://localhost URL,
|
|
requires daemon for push.
|
|
"""
|
|
semaphore = asyncio.Semaphore(jobs)
|
|
|
|
results = await asyncio.gather(
|
|
*[
|
|
_add_single_remote(
|
|
erplibre_root,
|
|
git_path,
|
|
p,
|
|
remote_name,
|
|
port,
|
|
remote_url_type,
|
|
semaphore,
|
|
)
|
|
for p in projects
|
|
]
|
|
)
|
|
|
|
added = results.count("added")
|
|
updated = results.count("updated")
|
|
errors = results.count("error")
|
|
print(
|
|
f"Remotes: {added} added, {updated} updated,"
|
|
f" {errors} errors"
|
|
)
|
|
|
|
|
|
# --- Async workers for push ---
|
|
|
|
|
|
async def _is_detached_head(repo_path):
|
|
"""Check if a repo is in detached HEAD state."""
|
|
_, _, rc = await _run_git(
|
|
"git", "-C", repo_path, "symbolic-ref", "HEAD"
|
|
)
|
|
return rc != 0
|
|
|
|
|
|
async def _checkout_manifest_branch(repo_path, revision):
|
|
"""Checkout the branch from the manifest revision.
|
|
|
|
When Google Repo syncs, repos end up in detached HEAD
|
|
on the exact commit. We create/checkout a local branch
|
|
matching the manifest revision so git push works.
|
|
"""
|
|
branch = (
|
|
revision.split("/")[-1] if "/" in revision else revision
|
|
)
|
|
|
|
# Check if local branch already exists
|
|
_, _, rc = await _run_git(
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"show-ref",
|
|
"--verify",
|
|
f"refs/heads/{branch}",
|
|
)
|
|
if rc == 0:
|
|
await _run_git(
|
|
"git", "-C", repo_path, "checkout", branch
|
|
)
|
|
else:
|
|
await _run_git(
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"checkout",
|
|
"-b",
|
|
branch,
|
|
)
|
|
return branch
|
|
|
|
|
|
async def _update_bare_head(git_path, project):
|
|
"""Update the HEAD of the bare repo to point to the
|
|
manifest branch, so git clone checks out the right
|
|
branch by default."""
|
|
repo_name = project["name"]
|
|
if not repo_name.endswith(".git"):
|
|
repo_name += ".git"
|
|
bare_path = os.path.join(git_path, repo_name)
|
|
if not os.path.isdir(bare_path):
|
|
return
|
|
|
|
revision = project.get("revision", "")
|
|
if not revision:
|
|
return
|
|
branch = (
|
|
revision.split("/")[-1] if "/" in revision else revision
|
|
)
|
|
await _run_git(
|
|
"git",
|
|
"-C",
|
|
bare_path,
|
|
"symbolic-ref",
|
|
"HEAD",
|
|
f"refs/heads/{branch}",
|
|
)
|
|
|
|
|
|
async def _try_unshallow(repo_path, project, remote_name):
|
|
"""Try to unshallow a repo by fetching full history."""
|
|
shallow_file = os.path.join(
|
|
repo_path, ".git", "shallow"
|
|
)
|
|
_logger.info(
|
|
f" Unshallowing {project['path']}..."
|
|
)
|
|
stdout, _, _ = await _run_git(
|
|
"git", "-C", repo_path, "remote"
|
|
)
|
|
remotes = [
|
|
r
|
|
for r in stdout.strip().split("\n")
|
|
if r and r != remote_name
|
|
]
|
|
manifest_remote = project.get("remote", "")
|
|
if manifest_remote in remotes:
|
|
remotes.remove(manifest_remote)
|
|
remotes.insert(0, manifest_remote)
|
|
|
|
for try_remote in remotes:
|
|
try:
|
|
await _run_git(
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"fetch",
|
|
"--unshallow",
|
|
try_remote,
|
|
timeout=300,
|
|
)
|
|
except asyncio.TimeoutError:
|
|
continue
|
|
if not os.path.exists(shallow_file):
|
|
_logger.info(
|
|
f" Unshallowed via {try_remote}"
|
|
)
|
|
return
|
|
if os.path.exists(shallow_file):
|
|
_logger.warning(
|
|
f" Unshallow failed for"
|
|
f" {project['path']},"
|
|
" falling back to shallow push"
|
|
)
|
|
|
|
|
|
async def _push_single_repo(
|
|
erplibre_root,
|
|
git_path,
|
|
project,
|
|
remote_name,
|
|
push_all_branches,
|
|
unshallow,
|
|
semaphore,
|
|
):
|
|
"""Push a single repo to local remote (async)."""
|
|
async with semaphore:
|
|
repo_path = os.path.join(
|
|
erplibre_root, project["path"]
|
|
)
|
|
if not os.path.isdir(repo_path):
|
|
_logger.warning(f" Not found: {repo_path}")
|
|
return "error", False
|
|
|
|
# Handle shallow repos
|
|
shallow_file = os.path.join(
|
|
repo_path, ".git", "shallow"
|
|
)
|
|
if os.path.exists(shallow_file):
|
|
if unshallow:
|
|
await _try_unshallow(
|
|
repo_path, project, remote_name
|
|
)
|
|
else:
|
|
# Enable shallow push on bare repo
|
|
repo_name = project["name"]
|
|
if not repo_name.endswith(".git"):
|
|
repo_name += ".git"
|
|
bare_path = os.path.join(
|
|
git_path, repo_name
|
|
)
|
|
if os.path.isdir(bare_path):
|
|
await _run_git(
|
|
"git",
|
|
"-C",
|
|
bare_path,
|
|
"config",
|
|
"receive.shallowUpdate",
|
|
"true",
|
|
)
|
|
_logger.info(
|
|
f" Shallow push for"
|
|
f" {project['path']}"
|
|
)
|
|
|
|
# Handle detached HEAD: checkout manifest branch
|
|
did_checkout = False
|
|
if await _is_detached_head(repo_path):
|
|
revision = project.get("revision", "")
|
|
if revision:
|
|
branch = await _checkout_manifest_branch(
|
|
repo_path, revision
|
|
)
|
|
_logger.info(
|
|
f" Checkout {branch} for"
|
|
f" {project['path']}"
|
|
" (was detached HEAD)"
|
|
)
|
|
did_checkout = True
|
|
else:
|
|
_logger.warning(
|
|
f" Detached HEAD with no revision"
|
|
f" for {project['path']}, skipping"
|
|
)
|
|
return "error", False
|
|
|
|
# Push
|
|
try:
|
|
if push_all_branches:
|
|
cmd = [
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"push",
|
|
remote_name,
|
|
"--all",
|
|
]
|
|
else:
|
|
cmd = [
|
|
"git",
|
|
"-C",
|
|
repo_path,
|
|
"push",
|
|
remote_name,
|
|
]
|
|
_, err, rc = await _run_git(
|
|
*cmd, timeout=120
|
|
)
|
|
if rc != 0:
|
|
_logger.warning(
|
|
f" Push failed for"
|
|
f" {project['path']}:"
|
|
f" {err.strip()}"
|
|
)
|
|
return "error", did_checkout
|
|
else:
|
|
await _update_bare_head(git_path, project)
|
|
_logger.info(
|
|
f" Pushed: {project['path']}"
|
|
)
|
|
return "pushed", did_checkout
|
|
except asyncio.TimeoutError:
|
|
_logger.warning(
|
|
f" Timeout pushing {project['path']}"
|
|
)
|
|
return "error", did_checkout
|
|
|
|
|
|
async def push_to_local(
|
|
erplibre_root,
|
|
git_path,
|
|
projects,
|
|
remote_name,
|
|
push_all_branches,
|
|
unshallow,
|
|
jobs,
|
|
):
|
|
"""Push to local remote for each repo (async)."""
|
|
semaphore = asyncio.Semaphore(jobs)
|
|
|
|
results = await asyncio.gather(
|
|
*[
|
|
_push_single_repo(
|
|
erplibre_root,
|
|
git_path,
|
|
p,
|
|
remote_name,
|
|
push_all_branches,
|
|
unshallow,
|
|
semaphore,
|
|
)
|
|
for p in projects
|
|
]
|
|
)
|
|
|
|
pushed = sum(1 for s, _ in results if s == "pushed")
|
|
errors = sum(1 for s, _ in results if s == "error")
|
|
checkouts = sum(1 for _, c in results if c)
|
|
print(
|
|
f"Push: {pushed} pushed, {checkouts} branch"
|
|
f" checkouts, {errors} errors"
|
|
)
|
|
|
|
|
|
# --- Serve (stays synchronous — long-running daemon) ---
|
|
|
|
|
|
def print_clone_commands(git_path, projects, port):
|
|
"""Print git clone commands for all available repos."""
|
|
print("=== Available repos to clone ===")
|
|
base_url = (
|
|
f"git://localhost:{port}"
|
|
if port != DEFAULT_PORT
|
|
else "git://localhost"
|
|
)
|
|
lines = []
|
|
for project in projects:
|
|
repo_name = project["name"]
|
|
if not repo_name.endswith(".git"):
|
|
repo_name += ".git"
|
|
bare_path = os.path.join(git_path, repo_name)
|
|
if os.path.isdir(bare_path):
|
|
clone_path = project["path"]
|
|
if clone_path == ".":
|
|
clone_path = "erplibre"
|
|
lines.append(
|
|
f" git clone {base_url}/{repo_name}"
|
|
f" {clone_path}"
|
|
)
|
|
lines.sort()
|
|
for line in lines:
|
|
print(line)
|
|
print(f"\nTotal: {len(lines)} repos available")
|
|
print()
|
|
|
|
|
|
def serve_git_daemon(git_path, projects, port):
|
|
"""Start git daemon to serve repos."""
|
|
print_clone_commands(git_path, projects, port)
|
|
|
|
cmd = (
|
|
f"git daemon --reuseaddr"
|
|
f" --base-path={git_path}"
|
|
f" --port={port}"
|
|
f" --export-all"
|
|
f" --enable=receive-pack"
|
|
f" {git_path}"
|
|
)
|
|
print(f"Starting git daemon on port {port}...")
|
|
print(f" Base path: {git_path}")
|
|
print(f" URL: git://localhost:{port}/")
|
|
print(" Press Ctrl+C to stop")
|
|
|
|
execute = Execute()
|
|
execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
# --- Main ---
|
|
|
|
|
|
async def run_actions(config, erplibre_root, projects):
|
|
"""Run init/remote/push actions asynchronously."""
|
|
action = config.action
|
|
jobs = config.jobs
|
|
|
|
if action in ("init", "all"):
|
|
print("=== Creating bare repos ===")
|
|
await init_bare_repos(config.path, projects, jobs)
|
|
print()
|
|
|
|
if action in ("remote", "all"):
|
|
print("=== Adding remotes ===")
|
|
await add_remotes(
|
|
erplibre_root,
|
|
config.path,
|
|
projects,
|
|
config.remote_name,
|
|
config.port,
|
|
config.remote_url_type,
|
|
jobs,
|
|
)
|
|
print()
|
|
|
|
if action in ("push", "all"):
|
|
print("=== Pushing to local ===")
|
|
await push_to_local(
|
|
erplibre_root,
|
|
config.path,
|
|
projects,
|
|
config.remote_name,
|
|
config.push_all_branches,
|
|
config.unshallow,
|
|
jobs,
|
|
)
|
|
print()
|
|
|
|
|
|
def main():
|
|
config = get_config()
|
|
|
|
log_level = logging.DEBUG if config.verbose else logging.INFO
|
|
logging.basicConfig(
|
|
level=log_level,
|
|
format="%(levelname)s: %(message)s",
|
|
)
|
|
|
|
# Check write permissions on target path
|
|
if not check_path_permissions(config.path):
|
|
require_permissions_or_exit(config.path)
|
|
|
|
# Detect ERPLibre root
|
|
if config.erplibre_root:
|
|
erplibre_root = os.path.abspath(config.erplibre_root)
|
|
else:
|
|
erplibre_root = DEFAULT_ERPLIBRE_PATH
|
|
|
|
manifest_path = os.path.join(erplibre_root, config.manifest)
|
|
if not os.path.exists(manifest_path):
|
|
print(f"Error: Manifest not found: {manifest_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"ERPLibre root: {erplibre_root}")
|
|
print(f"Manifest: {manifest_path}")
|
|
print(f"Git server path: {config.path}")
|
|
print(f"Remote name: {config.remote_name}")
|
|
if config.production_ready:
|
|
print("Mode: production (/srv/git)")
|
|
else:
|
|
print("Mode: development (~/.git-server)")
|
|
print(f"Remote URL type: {config.remote_url_type}")
|
|
print(f"Parallel jobs: {config.jobs}")
|
|
print()
|
|
|
|
projects = parse_manifest(manifest_path)
|
|
erplibre_project = get_erplibre_root_project(erplibre_root)
|
|
projects.append(erplibre_project)
|
|
print(
|
|
f"Found {len(projects)} projects"
|
|
f" ({len(projects) - 1} from manifest"
|
|
f" + erplibre root)"
|
|
)
|
|
print()
|
|
|
|
# Run async actions (init, remote, push)
|
|
asyncio.run(run_actions(config, erplibre_root, projects))
|
|
|
|
# Serve is synchronous (long-running daemon)
|
|
if config.action in ("serve", "all"):
|
|
print("=== Starting git daemon ===")
|
|
serve_git_daemon(config.path, projects, config.port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|