Created
June 26, 2026 09:35
-
-
Save lopesivan/09cc3efb65c94ef172ce12ee3a91955f to your computer and use it in GitHub Desktop.
repo no celular
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 | |
| """ | |
| adb-git.py — gerencia repositórios git em /sdcard/git via ADB (USB) | |
| uso: | |
| ./adb-git.py clone | |
| ./adb-git.py push <repo-local> [dest.git] | |
| ./adb-git.py pull <repo-local> | |
| ./adb-git.py ls | |
| """ | |
| import subprocess | |
| import sys | |
| import os | |
| import tempfile | |
| import shutil | |
| import argparse | |
| # ---------------------------------------------------------------------------- | |
| # config | |
| # ---------------------------------------------------------------------------- | |
| SDCARD_GIT = "/sdcard/git" | |
| RED = "\033[0;31m" | |
| GRN = "\033[0;32m" | |
| YLW = "\033[0;33m" | |
| BLU = "\033[0;34m" | |
| CYN = "\033[0;36m" | |
| BLD = "\033[1m" | |
| RST = "\033[0m" | |
| def info(msg): print(f"{BLU} ::{RST} {msg}") | |
| def ok(msg): print(f"{GRN} ✔ {RST} {msg}") | |
| def err(msg): print(f"{RED} ✘ {RST} {msg}", file=sys.stderr) | |
| def die(msg): err(msg); sys.exit(1) | |
| # ---------------------------------------------------------------------------- | |
| # adb helpers | |
| # ---------------------------------------------------------------------------- | |
| def adb(*args, capture=True, check=True): | |
| cmd = ["adb", *args] | |
| if capture: | |
| r = subprocess.run(cmd, capture_output=True, text=True) | |
| if check and r.returncode != 0: | |
| die(f"adb falhou: {' '.join(args)}\n{r.stderr.strip()}") | |
| return r | |
| else: | |
| r = subprocess.run(cmd) | |
| if check and r.returncode != 0: | |
| die(f"adb falhou: {' '.join(args)}") | |
| return r | |
| def check_device(): | |
| r = adb("devices") | |
| lines = [l for l in r.stdout.strip().splitlines()[1:] if l.strip()] | |
| if not lines: | |
| die("nenhum dispositivo ADB conectado") | |
| dev = lines[0].split()[0] | |
| info(f"dispositivo: {dev}") | |
| def adb_ls_repos(): | |
| """lista todos os .git em SDCARD_GIT recursivamente""" | |
| r = adb("shell", f"find {SDCARD_GIT} -type d -name '*.git'") | |
| repos = sorted([l.strip() for l in r.stdout.splitlines() if l.strip()]) | |
| return repos | |
| # ---------------------------------------------------------------------------- | |
| # fzf helper | |
| # ---------------------------------------------------------------------------- | |
| def fzf_select(items, prompt="repo > ", header="selecione"): | |
| if not shutil.which("fzf"): | |
| # fallback numérico | |
| print(f"\n{BLD}{header}{RST}") | |
| for i, item in enumerate(items): | |
| print(f" {CYN}{i+1:3}{RST} {item}") | |
| try: | |
| ans = input("\nnúmero: ").strip() | |
| idx = int(ans) - 1 | |
| if 0 <= idx < len(items): | |
| return items[idx] | |
| except (ValueError, KeyboardInterrupt): | |
| pass | |
| return None | |
| joined = "\n".join(items) | |
| r = subprocess.run( | |
| ["fzf", | |
| f"--prompt= {prompt}", | |
| f"--header= {header}", | |
| "--height=50%", | |
| "--border", | |
| "--preview=echo {}", | |
| "--preview-window=up:1"], | |
| input=joined, capture_output=True, text=True | |
| ) | |
| return r.stdout.strip() if r.returncode == 0 else None | |
| # ---------------------------------------------------------------------------- | |
| # operações | |
| # ---------------------------------------------------------------------------- | |
| def cmd_ls(): | |
| """lista repositórios no celular""" | |
| check_device() | |
| repos = adb_ls_repos() | |
| if not repos: | |
| die(f"nenhum .git encontrado em {SDCARD_GIT}") | |
| info(f"{len(repos)} repositório(s) em {SDCARD_GIT}:") | |
| for r in repos: | |
| rel = r.replace(SDCARD_GIT + "/", "") | |
| print(f" {CYN}{rel}{RST}") | |
| def cmd_clone(): | |
| """seleciona repo via fzf e clona localmente via adb pull""" | |
| check_device() | |
| repos = adb_ls_repos() | |
| if not repos: | |
| die(f"nenhum .git encontrado em {SDCARD_GIT}") | |
| info(f"{len(repos)} repositório(s) encontrado(s)") | |
| selected = fzf_select( | |
| repos, | |
| prompt="clone > ", | |
| header=f"selecione repositório para clonar (ESC para sair)" | |
| ) | |
| if not selected: | |
| info("cancelado.") | |
| sys.exit(0) | |
| name = os.path.basename(selected) # ex: foo.git | |
| tmpdir = tempfile.mkdtemp(prefix="adb-git-") | |
| local_bare = os.path.join(tmpdir, name) | |
| dest_name = name.replace(".git", "") # foo | |
| dest_dir = os.path.join(os.getcwd(), dest_name) # ./foo | |
| if os.path.exists(dest_dir): | |
| die(f"destino já existe: {dest_dir}") | |
| print() | |
| print(f"{BLD}repositório{RST} : {CYN}{selected}{RST}") | |
| print(f"{BLD}destino{RST} : {CYN}{dest_dir}{RST}") | |
| print() | |
| ans = input("confirma clone? [S/n] ").strip() | |
| if ans and ans.lower() not in ("s", ""): | |
| info("cancelado.") | |
| sys.exit(0) | |
| # 1) pull do .git bare para /tmp | |
| if os.path.exists(local_bare): | |
| shutil.rmtree(local_bare) | |
| info(f"transferindo via adb pull → {local_bare} ...") | |
| adb("pull", selected, local_bare, capture=False) | |
| # 2) git clone --local do bare para o destino | |
| info(f"clonando local → {dest_dir} ...") | |
| r = subprocess.run( | |
| ["git", "clone", "--local", "--no-hardlinks", local_bare, dest_dir]) | |
| if r.returncode != 0: | |
| die("git clone falhou") | |
| # 3) limpa bare temporário | |
| shutil.rmtree(local_bare, ignore_errors=True) | |
| print() | |
| ok(f"pronto: {dest_dir}") | |
| print(f"\n {CYN}cd {dest_name}{RST}\n") | |
| def cmd_push(dest_hint=None): | |
| """ | |
| envia repo do cwd para o celular. | |
| dest_hint pode ser: | |
| - None → busca pelo nome do cwd | |
| - "local-kiko" → categoria (monta o path completo) | |
| - "/sdcard/git/.../foo.git" → path absoluto | |
| """ | |
| check_device() | |
| repo_local = os.getcwd() | |
| name = os.path.basename(repo_local) | |
| # resolve o bare remoto | |
| if dest_hint is None: | |
| repos = adb_ls_repos() | |
| matches = [r for r in repos if os.path.basename(r) == f"{name}.git"] | |
| if not matches: | |
| die( | |
| f"não encontrei {name}.git no celular.\n" | |
| f" crie com: adb-git init {name} <categoria>" | |
| ) | |
| if len(matches) > 1: | |
| selected = fzf_select(matches, prompt="push > ", | |
| header="múltiplos destinos — selecione") | |
| if not selected: | |
| die("cancelado") | |
| remote_bare = selected | |
| else: | |
| remote_bare = matches[0] | |
| elif dest_hint.startswith("/"): | |
| # path absoluto | |
| remote_bare = dest_hint | |
| else: | |
| # categoria: ex "local-kiko" ou "kiko" | |
| category = dest_hint if dest_hint.startswith( | |
| "local-") else f"local-{dest_hint}" | |
| remote_bare = f"{SDCARD_GIT}/{category}/{name}.git" | |
| name = os.path.basename(repo_local) | |
| local_bare_tmp = f"/tmp/{name}_remote.git" | |
| # 1) baixa o bare remoto para /tmp | |
| if os.path.exists(local_bare_tmp): | |
| shutil.rmtree(local_bare_tmp) | |
| info(f"baixando bare remoto → {local_bare_tmp} ...") | |
| adb("pull", remote_bare, local_bare_tmp, capture=False) | |
| # 2) push do repo local para o bare temporário (tudo local, sem Android) | |
| info(f"aplicando commits no bare local ...") | |
| r = subprocess.run([ | |
| "git", "-C", repo_local, | |
| "push", "--all", f"file://{local_bare_tmp}" | |
| ]) | |
| if r.returncode != 0: | |
| shutil.rmtree(local_bare_tmp, ignore_errors=True) | |
| die("git push para bare local falhou") | |
| # 3) envia bare atualizado de volta para o celular | |
| info(f"enviando bare atualizado via adb push → {remote_bare} ...") | |
| adb("push", local_bare_tmp + "/.", remote_bare, capture=False) | |
| shutil.rmtree(local_bare_tmp, ignore_errors=True) | |
| print() | |
| ok(f"push concluído → {remote_bare}") | |
| def cmd_pull(repo_local): | |
| """ | |
| atualiza repo local buscando do celular via adb pull do bare | |
| """ | |
| check_device() | |
| repo_local = os.path.abspath(repo_local) | |
| if not os.path.isdir(repo_local): | |
| die(f"diretório não encontrado: {repo_local}") | |
| name = os.path.basename(repo_local) | |
| repos = adb_ls_repos() | |
| matches = [r for r in repos if os.path.basename(r) == f"{name}.git"] | |
| if not matches: | |
| die(f"não encontrei {name}.git no celular") | |
| remote_bare = matches[0] if len(matches) == 1 else fzf_select( | |
| matches, prompt="pull > ", header="selecione origem" | |
| ) | |
| if not remote_bare: | |
| die("cancelado") | |
| local_bare_tmp = f"/tmp/{name}_remote.git" | |
| # 1) baixa o bare do celular | |
| if os.path.exists(local_bare_tmp): | |
| shutil.rmtree(local_bare_tmp) | |
| info(f"baixando bare remoto → {local_bare_tmp} ...") | |
| adb("pull", remote_bare, local_bare_tmp, capture=False) | |
| # 2) fetch do bare local para o repo de trabalho | |
| info(f"aplicando commits em {repo_local} ...") | |
| r = subprocess.run([ | |
| "git", "-C", repo_local, | |
| "fetch", f"file://{local_bare_tmp}", "refs/heads/*:refs/remotes/phone/*" | |
| ]) | |
| if r.returncode != 0: | |
| shutil.rmtree(local_bare_tmp, ignore_errors=True) | |
| die("git fetch falhou") | |
| shutil.rmtree(local_bare_tmp, ignore_errors=True) | |
| # detecta branch principal | |
| r2 = subprocess.run( | |
| ["git", "-C", repo_local, "symbolic-ref", "--short", "HEAD"], | |
| capture_output=True, text=True | |
| ) | |
| branch = r2.stdout.strip() or "main" | |
| print() | |
| ok("pull concluído — branches disponíveis como remotes/phone/*") | |
| print(f" para merge: {CYN}git merge remotes/phone/{branch}{RST}") | |
| print() | |
| def cmd_remove(): | |
| """ | |
| remove um bare .git do celular em /sdcard/git usando seleção via fzf | |
| """ | |
| check_device() | |
| repos = adb_ls_repos() | |
| if not repos: | |
| die(f"nenhum .git encontrado em {SDCARD_GIT}") | |
| selected = fzf_select( | |
| repos, | |
| prompt="remove > ", | |
| header="selecione repositório para REMOVER do celular" | |
| ) | |
| if not selected: | |
| info("cancelado.") | |
| sys.exit(0) | |
| print() | |
| print(f"{BLD}remover{RST}: {RED}{selected}{RST}") | |
| print() | |
| ans = input("confirma remoção definitiva? [S/n] ").strip() | |
| if ans and ans.lower() not in ("s", ""): | |
| info("cancelado.") | |
| sys.exit(0) | |
| adb("shell", f"rm -rf '{selected}'", capture=False) | |
| print() | |
| ok(f"removido: {selected}") | |
| def cmd_init(name, category="local"): | |
| """ | |
| cria um novo bare .git no celular sem precisar de git no Android. | |
| estratégia: git init --bare local → adb push para o celular. | |
| """ | |
| check_device() | |
| bare_name = name if name.endswith(".git") else f"{name}.git" | |
| repo_name = bare_name.replace(".git", "") | |
| # prefixo local- se não tiver | |
| if not category.startswith("local-"): | |
| category = f"local-{category}" | |
| remote_path = f"{SDCARD_GIT}/{category}/{bare_name}" | |
| local_bare_tmp = f"/tmp/{bare_name}" | |
| # verifica se já existe no celular | |
| r = adb( | |
| "shell", f"[ -d {remote_path} ] && echo exists || echo absent", check=False) | |
| if r.stdout.strip() == "exists": | |
| die(f"já existe no celular: {remote_path}") | |
| # cria bare localmente | |
| if os.path.exists(local_bare_tmp): | |
| shutil.rmtree(local_bare_tmp) | |
| info(f"criando bare local → {local_bare_tmp} ...") | |
| r = subprocess.run(["git", "init", "--bare", local_bare_tmp]) | |
| if r.returncode != 0: | |
| die("git init --bare falhou") | |
| # cria a pasta de categoria no celular se não existir | |
| category_path = f"{SDCARD_GIT}/{category}" | |
| adb("shell", f"mkdir -p {category_path}", check=False) | |
| # envia bare para o celular | |
| info(f"enviando para o celular → {remote_path} ...") | |
| adb("push", local_bare_tmp + "/.", remote_path, capture=False) | |
| shutil.rmtree(local_bare_tmp, ignore_errors=True) | |
| print() | |
| ok(f"criado: {remote_path}") | |
| print(f"\n agora faça o primeiro push:") | |
| print(f" {CYN}cd {repo_name} && adb-git push {category}{RST}\n") | |
| # ---------------------------------------------------------------------------- | |
| # main | |
| # ---------------------------------------------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="gerencia repos git em /sdcard/git via ADB" | |
| ) | |
| sub = parser.add_subparsers(dest="cmd") | |
| sub.add_parser("ls", help="lista repositórios no celular") | |
| sub.add_parser("clone", help="seleciona e clona repositório") | |
| sub.add_parser("remove", help="remove um repositório .git do celular") | |
| p_push = sub.add_parser("push", help="envia repo local para o celular") | |
| p_push.add_argument("dest_git", nargs="?", | |
| help="categoria ou caminho .git no celular (opcional, padrão: detecta pelo nome)") | |
| p_pull = sub.add_parser( | |
| "pull", help="atualiza repo local a partir do celular") | |
| p_pull.add_argument("repo", nargs="?", default=None, | |
| help="caminho do repo local (padrão: diretório atual)") | |
| p_init = sub.add_parser("init", help="cria novo bare .git no celular") | |
| p_init.add_argument("name", nargs="?", default=None, | |
| help="nome do repositório (padrão: nome do diretório atual)") | |
| p_init.add_argument("category", nargs="?", default=None, | |
| help="categoria/subpasta (padrão: local)") | |
| args = parser.parse_args() | |
| if args.cmd == "ls": | |
| cmd_ls() | |
| elif args.cmd == "clone": | |
| cmd_clone() | |
| elif args.cmd == "push": | |
| cmd_push(args.dest_git) | |
| elif args.cmd == "pull": | |
| cmd_pull(args.repo or os.getcwd()) | |
| elif args.cmd == "remove": | |
| cmd_remove() | |
| elif args.cmd == "init": | |
| cwd = os.path.basename(os.getcwd()) | |
| if args.category is None and args.name and args.name.startswith("local-"): | |
| cmd_init(cwd, args.name) | |
| else: | |
| cmd_init(args.name or cwd, args.category or "local") | |
| else: | |
| parser.print_help() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment