Automatizando Git com Python

Automatize tarefas do Git com Python usando GitPython e subprocess. Crie scripts para commits, branches, relatorios e fluxos CI/CD.

5 min de leitura Equipe Python Brasil

Git e a ferramenta de controle de versao mais utilizada no mundo, e Python pode automatizar praticamente qualquer fluxo de trabalho com ele. Desde gerar relatorios de commits ate criar hooks personalizados e gerenciar repositorios em massa, a combinacao de Python e Git e extremamente poderosa. Neste artigo, vamos explorar como automatizar Git usando GitPython e subprocess.

Instalando o GitPython

O GitPython e a biblioteca mais popular para interagir com repositorios Git em Python:

pip install gitpython

Vamos comecar com operacoes basicas:

from git import Repo

# Abrir repositorio existente
repo = Repo("/caminho/do/projeto")

# Informacoes basicas
print(f"Branch atual: {repo.active_branch.name}")
print(f"Ultimo commit: {repo.head.commit.hexsha[:8]}")
print(f"Autor: {repo.head.commit.author.name}")
print(f"Mensagem: {repo.head.commit.message.strip()}")
print(f"Tem alteracoes? {repo.is_dirty()}")
print(f"Arquivos nao rastreados: {repo.untracked_files}")

Automatizando Commits e Branches

Crie scripts para operacoes comuns de Git:

from git import Repo
from datetime import datetime

def commit_automatico(caminho_repo: str, mensagem: str = None):
    """Adiciona todas as alteracoes e faz commit."""
    repo = Repo(caminho_repo)

    if not repo.is_dirty() and not repo.untracked_files:
        print("Nenhuma alteracao para commitar.")
        return None

    # Adicionar todas as alteracoes
    repo.git.add(A=True)

    # Gerar mensagem automatica se nao fornecida
    if mensagem is None:
        alterados = [item.a_path for item in repo.index.diff("HEAD")]
        novos = repo.untracked_files
        mensagem = f"auto: {len(alterados)} alterados, {len(novos)} novos [{datetime.now().strftime('%Y-%m-%d %H:%M')}]"

    # Commit
    commit = repo.index.commit(mensagem)
    print(f"Commit realizado: {commit.hexsha[:8]} - {mensagem}")
    return commit

def criar_branch_feature(caminho_repo: str, nome_feature: str):
    """Cria branch de feature a partir da main."""
    repo = Repo(caminho_repo)
    branch_nome = f"feature/{nome_feature}"

    # Garantir que estamos na main atualizada
    main = repo.heads.main
    main.checkout()
    repo.remotes.origin.pull()

    # Criar e mudar para nova branch
    nova_branch = repo.create_head(branch_nome)
    nova_branch.checkout()
    print(f"Branch criada e ativa: {branch_nome}")
    return nova_branch

def listar_branches_remotas(caminho_repo: str) -> list[str]:
    """Lista todas as branches remotas."""
    repo = Repo(caminho_repo)
    repo.remotes.origin.fetch()

    branches = []
    for ref in repo.remotes.origin.refs:
        branches.append(ref.name.replace("origin/", ""))

    return branches

# Uso
commit_automatico("/caminho/do/projeto", "feat: adiciona validacao de email")

Gerando Relatorios de Commits

Analise o historico do repositorio para gerar relatorios uteis:

from git import Repo
from collections import defaultdict, Counter
from datetime import datetime, timedelta

def relatorio_semanal(caminho_repo: str, dias: int = 7) -> dict:
    """Gera relatorio dos commits dos ultimos N dias."""
    repo = Repo(caminho_repo)
    data_limite = datetime.now() - timedelta(days=dias)

    commits_por_autor = defaultdict(list)
    arquivos_alterados = Counter()
    total_commits = 0

    for commit in repo.iter_commits("main"):
        data_commit = datetime.fromtimestamp(commit.committed_date)
        if data_commit < data_limite:
            break

        total_commits += 1
        autor = commit.author.name
        commits_por_autor[autor].append({
            "hash": commit.hexsha[:8],
            "mensagem": commit.message.strip(),
            "data": data_commit.strftime("%Y-%m-%d %H:%M"),
        })

        # Contar arquivos alterados
        for arquivo in commit.stats.files:
            arquivos_alterados[arquivo] += 1

    # Montar relatorio
    relatorio = {
        "periodo": f"Ultimos {dias} dias",
        "total_commits": total_commits,
        "autores": {},
        "top_arquivos": arquivos_alterados.most_common(10),
    }

    for autor, commits in commits_por_autor.items():
        relatorio["autores"][autor] = {
            "total": len(commits),
            "commits": commits,
        }

    return relatorio

def imprimir_relatorio(relatorio: dict):
    """Imprime o relatorio formatado."""
    print(f"Relatorio de Commits - {relatorio['periodo']}")
    print(f"Total de commits: {relatorio['total_commits']}")
    print()

    print("Commits por autor:")
    for autor, dados in relatorio["autores"].items():
        print(f"  {autor}: {dados['total']} commits")
        for c in dados["commits"][:3]:
            print(f"    [{c['hash']}] {c['mensagem']}")

    print("\nArquivos mais alterados:")
    for arquivo, contagem in relatorio["top_arquivos"]:
        print(f"  {arquivo}: {contagem} alteracoes")

rel = relatorio_semanal("/caminho/do/projeto", dias=7)
imprimir_relatorio(rel)

Git Hooks com Python

Hooks sao scripts executados automaticamente em eventos do Git. Vamos criar hooks uteis em Python:

#!/usr/bin/env python3
"""
pre-commit hook: verifica qualidade do codigo antes do commit.
Salvar em .git/hooks/pre-commit e tornar executavel.
"""
import subprocess
import sys

def verificar_formatacao():
    """Verifica se o codigo esta formatado com black."""
    resultado = subprocess.run(
        ["black", "--check", "."],
        capture_output=True,
        text=True,
    )
    if resultado.returncode != 0:
        print("ERRO: Codigo nao formatado. Execute 'black .' antes do commit.")
        print(resultado.stdout)
        return False
    return True

def verificar_linting():
    """Executa flake8 nos arquivos staged."""
    resultado = subprocess.run(
        ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
        capture_output=True,
        text=True,
    )
    arquivos_python = [
        f for f in resultado.stdout.strip().split("\n")
        if f.endswith(".py")
    ]

    if not arquivos_python:
        return True

    resultado = subprocess.run(
        ["flake8"] + arquivos_python,
        capture_output=True,
        text=True,
    )
    if resultado.returncode != 0:
        print("ERRO: Problemas de linting encontrados:")
        print(resultado.stdout)
        return False
    return True

def verificar_testes():
    """Executa testes rapidos."""
    resultado = subprocess.run(
        ["python", "-m", "pytest", "tests/", "-x", "--timeout=30"],
        capture_output=True,
        text=True,
    )
    if resultado.returncode != 0:
        print("ERRO: Testes falharam:")
        print(resultado.stdout[-500:])
        return False
    return True

if __name__ == "__main__":
    verificacoes = [
        ("Formatacao", verificar_formatacao),
        ("Linting", verificar_linting),
        ("Testes", verificar_testes),
    ]

    falhou = False
    for nome, funcao in verificacoes:
        print(f"Verificando {nome}...", end=" ")
        if funcao():
            print("OK")
        else:
            print("FALHOU")
            falhou = True

    if falhou:
        print("\nCommit bloqueado. Corrija os problemas acima.")
        sys.exit(1)

    print("\nTodas as verificacoes passaram!")

Gerenciamento em Massa de Repositorios

Para equipes com muitos repos, automatize operacoes em massa:

import os
from git import Repo, GitCommandError
from pathlib import Path

def atualizar_todos_repos(pasta_base: str):
    """Faz pull em todos os repositorios de uma pasta."""
    resultados = {"sucesso": [], "erro": [], "limpo": []}

    for item in Path(pasta_base).iterdir():
        if not item.is_dir():
            continue

        git_dir = item / ".git"
        if not git_dir.exists():
            continue

        try:
            repo = Repo(str(item))
            branch = repo.active_branch.name

            if repo.is_dirty():
                resultados["erro"].append(
                    f"{item.name}: alteracoes nao commitadas"
                )
                continue

            origin = repo.remotes.origin
            info = origin.pull()
            if info[0].flags & info[0].HEAD_UPTODATE:
                resultados["limpo"].append(item.name)
            else:
                resultados["sucesso"].append(f"{item.name} ({branch})")

        except GitCommandError as e:
            resultados["erro"].append(f"{item.name}: {e}")
        except Exception as e:
            resultados["erro"].append(f"{item.name}: {e}")

    # Relatorio
    print(f"Atualizados: {len(resultados['sucesso'])}")
    for r in resultados["sucesso"]:
        print(f"  {r}")

    print(f"\nJa atualizados: {len(resultados['limpo'])}")

    if resultados["erro"]:
        print(f"\nErros: {len(resultados['erro'])}")
        for e in resultados["erro"]:
            print(f"  {e}")

atualizar_todos_repos("/home/dev/projetos/")

Usando Subprocess para Comandos Git Avancados

Quando o GitPython nao cobre um caso especifico, use subprocess:

import subprocess
import json

def git_log_json(caminho: str, limite: int = 20) -> list[dict]:
    """Obtem log do git em formato estruturado."""
    formato = '{"hash": "%H", "autor": "%an", "email": "%ae", "data": "%ai", "mensagem": "%s"}'

    resultado = subprocess.run(
        ["git", "-C", caminho, "log", f"--max-count={limite}", f"--format={formato}"],
        capture_output=True,
        text=True,
    )

    commits = []
    for linha in resultado.stdout.strip().split("\n"):
        if linha:
            commits.append(json.loads(linha))

    return commits

def buscar_em_historico(caminho: str, termo: str) -> list[str]:
    """Busca um termo em todo o historico de commits."""
    resultado = subprocess.run(
        ["git", "-C", caminho, "log", "--all", f"--grep={termo}", "--oneline"],
        capture_output=True,
        text=True,
    )
    return resultado.stdout.strip().split("\n")

commits = git_log_json("/caminho/do/projeto", limite=10)
for c in commits:
    print(f"[{c['hash'][:8]}] {c['autor']}: {c['mensagem']}")

Boas Praticas

Ao automatizar Git com Python, sempre trate excecoes do GitPython adequadamente. Nunca armazene credenciais no codigo; use SSH keys ou credential helpers. Teste seus scripts em repositorios de teste antes de aplica-los em producao. E documente os hooks e scripts para que toda a equipe entenda o fluxo automatizado.

Conclusao

Automatizar Git com Python elimina tarefas repetitivas e reduz erros humanos no fluxo de desenvolvimento. Desde hooks de pre-commit que garantem qualidade ate relatorios automaticos e gerenciamento em massa, as possibilidades sao vastas. Comece pelos hooks mais simples e evolua para automacoes completas conforme a necessidade da sua equipe.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português