Skip to content

Instantly share code, notes, and snippets.

@lopesivan
Created June 26, 2026 09:35
Show Gist options
  • Select an option

  • Save lopesivan/09cc3efb65c94ef172ce12ee3a91955f to your computer and use it in GitHub Desktop.

Select an option

Save lopesivan/09cc3efb65c94ef172ce12ee3a91955f to your computer and use it in GitHub Desktop.
repo no celular
#!/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