Fixar o âmbito e observar três referências
O laboratório completo abaixo cria um remoto bare e dois clones, A e B, dentro de uma pasta temporária. Só permite transporte file, ignora configuração global e de sistema e usa uma pasta de hooks vazia. Não usa o origin do projeto nem um serviço alojado. A cria O; B obtém O, cria C e publica C no remoto local. Antes de qualquer fetch, origin/main de A ainda aponta para O. A seguir, A cria L a partir de O. Desenha O com dois descendentes, C e L, e identifica em cada momento HEAD de A, origin/main de A e main no remoto. O desenho evita tratar três referências como uma única realidade. Guarda o código num ficheiro run.py e executa com Python 3.13 no ambiente indicado. O runner exige /usr/bin/git com versão 2.50.1 Apple Git-155.
Medir uma rejeição e o efeito de fetch
O push normal de L é rejeitado porque o remoto já contém C. O runner verifica o código de saída e lê novamente a referência no remoto para confirmar que C ficou intacto. Depois faz fetch em A: origin/main passa a C, mas HEAD e value.txt continuam em L. A contagem da diferença simétrica main...origin/main é um commit exclusivo de cada lado. Estes resultados não implicam conflito textual, nem avaliam a qualidade das duas alterações. Permitem apenas descrever a divergência com precisão antes de escolher integração. Numa situação fictícia de suporte, um operador pode ter um hotfix pronto enquanto outro publicou uma alteração de batch. O primeiro passo útil é identificar os dois conteúdos e os consumidores, em vez de tentar repetidamente o mesmo push. A decisão funcional continua fora do mecanismo de transferência de objetos.
Distinguir condição de concorrência e preservação
O exercício tenta depois um lease explícito contra O, embora fetch já tenha atualizado origin/main para C. A tentativa é rejeitada e o remoto permanece em C. Para demonstrar o limite da proteção, o guião autoriza uma reescrita apenas no seu remoto descartável, indicando C como valor esperado. A atualização para L é aceite. Imediatamente a seguir, uma consulta de ancestralidade confirma que C não é antecessor de L. Portanto, aceitar a condição não demonstra inclusão do trabalho substituído. No caso Âncora, uma revisão feita contra O não cobre automaticamente C. Se a referência mudou, revê a nova história antes de renovar a decisão. Não mudes apenas o valor esperado para eliminar o erro. Num serviço alojado ainda existem permissões, regras de proteção e verificações que este laboratório não executa.
Limpar referências com o âmbito explícito
A última parte de colaboração cria temporary no remoto. B obtém a referência, cria retained localmente e guarda uma tag local-only. Depois de apagar temporary no remoto, B ainda vê origin/temporary até executar fetch --prune. Com o refspec normal do clone usado aqui, a referência de acompanhamento desaparece; retained e local-only continuam a apontar para O. Não generalizes este resultado a opções que acrescentam pruning de tags ou a configurações mirror. O caso Cedro pede precisamente essa revisão de âmbito. O runner termina com vinte e quatro verificações que incluem também os ensaios de bisect da aula seguinte. Remove os repositórios temporários ao fechar. As observações aplicam-se ao binário executado; a documentação atual consultada pertence à linha 2.56. Nenhum teste foi realizado com autenticação, hooks do servidor, branch protection ou aplicações reais.
"""DR original local Git experiments. Python 3.13 and Apple Git 2.50.1.
Run: python3 run.py
All repositories, remote refs and rewrites are disposable and local.
No project repositories, network hosts, credentials or personal Git config.
"""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
GIT = '/usr/bin/git'
version = subprocess.check_output([GIT, '--version'], text=True).strip()
assert version == 'git version 2.50.1 (Apple Git-155)', version
checks = []
def check(name, value):
assert value, name
checks.append(name)
with tempfile.TemporaryDirectory(prefix='dr-git-diagnosis-') as directory:
root = Path(directory)
hooks = root / 'empty-hooks'
hooks.mkdir()
template = root / 'empty-template'
template.mkdir()
env = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')}
env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,
GIT_ALLOW_PROTOCOL='file', GIT_TERMINAL_PROMPT='0',
GIT_AUTHOR_NAME='DR Synthetic', GIT_AUTHOR_EMAIL='lab@example.test',
GIT_COMMITTER_NAME='DR Synthetic', GIT_COMMITTER_EMAIL='lab@example.test',
GIT_AUTHOR_DATE='2026-10-03T12:00:00+00:00',
GIT_COMMITTER_DATE='2026-10-03T12:00:00+00:00', LC_ALL='C')
base = [GIT, '-c', 'core.hooksPath=' + str(hooks), '-c', 'commit.gpgSign=false',
'-c', 'tag.gpgSign=false', '-c', 'init.templateDir=' + str(template),
'-c', 'core.autocrlf=false', '-c', 'core.fsmonitor=false']
def command(repo, *args, ok=True):
result = subprocess.run(base + list(args), cwd=repo, env=env,
capture_output=True, text=True, timeout=15)
if ok and result.returncode:
raise AssertionError((args, result.returncode, result.stdout, result.stderr))
return result
def text(repo, *args):
return command(repo, *args).stdout.strip()
def commit(repo, value, message):
(repo / 'value.txt').write_text(value + '\n')
command(repo, 'add', 'value.txt')
command(repo, 'commit', '-m', message)
return text(repo, 'rev-parse', 'HEAD')
remote = root / 'remote.git'
command(root, 'init', '--bare', '--initial-branch=main', str(remote))
a = root / 'a'
b = root / 'b'
command(root, 'clone', str(remote), str(a))
original = commit(a, 'base', 'initial synthetic state')
command(a, 'push', 'origin', 'main')
command(root, 'clone', str(remote), str(b))
concurrent = commit(b, 'concurrent', 'colleague synthetic change')
command(b, 'push', 'origin', 'main')
check('remote: tracking ref remains stale until fetch', text(a, 'rev-parse', 'origin/main') == original)
local = commit(a, 'local', 'independent synthetic change')
rejection = command(a, 'push', 'origin', 'main', ok=False)
check('remote: ordinary divergent push is rejected', rejection.returncode != 0)
check('remote: rejected push preserves remote tip', text(remote, 'rev-parse', 'main') == concurrent)
command(a, 'fetch', 'origin')
check('fetch: remote-tracking ref advances', text(a, 'rev-parse', 'origin/main') == concurrent)
check('fetch: current branch and worktree remain local',
text(a, 'rev-parse', 'HEAD') == local and (a / 'value.txt').read_text() == 'local\n')
check('fetch: symmetric difference contains one commit per side',
text(a, 'rev-list', '--left-right', '--count', 'main...origin/main').split() == ['1', '1'])
stale = command(a, 'push', '--force-with-lease=refs/heads/main:' + original,
'origin', 'HEAD:refs/heads/main', ok=False)
check('lease: explicit old expectation rejects after fetch', stale.returncode != 0)
check('lease: stale expectation leaves colleague tip intact', text(remote, 'rev-parse', 'main') == concurrent)
# Deliberately authorized rewrite of only this disposable local remote.
command(a, 'push', '--force-with-lease=refs/heads/main:' + concurrent,
'origin', 'HEAD:refs/heads/main')
check('lease: matching expectation permits this local rewrite', text(remote, 'rev-parse', 'main') == local)
check('lease: accepted rewrite need not include overwritten commit',
command(a, 'merge-base', '--is-ancestor', concurrent, local, ok=False).returncode == 1)
command(a, 'branch', 'temporary', original)
command(a, 'push', 'origin', 'temporary')
command(b, 'fetch', 'origin')
command(b, 'branch', 'retained', 'origin/temporary')
command(b, 'tag', 'local-only', original)
command(a, 'push', 'origin', '--delete', 'temporary')
check('prune: deleted remote branch initially remains in tracking refs',
text(b, 'rev-parse', 'origin/temporary') == original)
command(b, 'fetch', '--prune', 'origin')
check('prune: default clone refspec removes stale remote-tracking ref',
command(b, 'show-ref', '--verify', '--quiet', 'refs/remotes/origin/temporary', ok=False).returncode == 1)
check('prune: local branch remains', text(b, 'rev-parse', 'retained') == original)
check('prune: local-only tag remains without tag refspec', text(b, 'rev-parse', 'local-only') == original)
history = root / 'history'
command(root, 'init', '--initial-branch=main', str(history))
commits = []
for n in range(9):
(history / 'sample.json').write_text(json.dumps({'revision': n, 'result': 'bad' if n >= 5 else 'good'}))
command(history, 'add', 'sample.json')
command(history, 'commit', '-m', 'synthetic revision ' + str(n))
commits.append(text(history, 'rev-parse', 'HEAD'))
oracle = root / 'oracle.py'
oracle.write_text("import json,sys\nfrom pathlib import Path\nr=json.loads(Path('sample.json').read_text())\n"
"mode=sys.argv[1]\n"
"if mode=='abort': sys.exit(128)\n"
"if mode=='skip' and r['revision'] in (4,5,6): sys.exit(125)\n"
"sys.exit(1 if r['result']=='bad' else 0)\n")
good_result = subprocess.run([sys.executable, str(oracle), 'normal'], cwd=history,
capture_output=True)
check('bisect: bad endpoint fails original oracle', good_result.returncode == 1)
command(history, 'switch', '--detach', commits[0])
good_result = subprocess.run([sys.executable, str(oracle), 'normal'], cwd=history,
capture_output=True)
check('bisect: good endpoint passes original oracle', good_result.returncode == 0)
command(history, 'switch', 'main')
command(history, 'bisect', 'start', commits[8], commits[0])
command(history, 'bisect', 'run', sys.executable, str(oracle), 'normal')
check('bisect: stable oracle identifies revision five', text(history, 'rev-parse', 'refs/bisect/bad') == commits[5])
log = text(history, 'bisect', 'log')
check('bisect: log records observed endpoints and conclusion', commits[0] in log and commits[8] in log and commits[5] in log)
logfile = root / 'bisect.log'
logfile.write_text(log + '\n')
command(history, 'bisect', 'reset')
check('bisect: reset returns to original branch and tip',
text(history, 'symbolic-ref', '--short', 'HEAD') == 'main' and text(history, 'rev-parse', 'HEAD') == commits[8])
command(history, 'bisect', 'replay', str(logfile))
check('bisect: replay reconstructs recorded conclusion', text(history, 'rev-parse', 'refs/bisect/bad') == commits[5])
command(history, 'bisect', 'reset')
command(history, 'bisect', 'start', commits[8], commits[0])
skipped = command(history, 'bisect', 'run', sys.executable, str(oracle), 'skip', ok=False)
output = skipped.stdout + skipped.stderr
candidates = [n for n, oid in enumerate(commits) if oid in output]
# The unresolved boundary contains skipped revisions 4,5,6 and tested-bad 7.
check('bisect: skipping boundary prevents a unique conclusion', skipped.returncode != 0 and 'first bad commit could be any of' in output)
check('bisect: ambiguous output includes actual culprit and adjacent candidates',
all(commits[n] in output for n in (4,5,6,7)))
command(history, 'bisect', 'reset')
command(history, 'bisect', 'start', commits[8], commits[0])
aborted = command(history, 'bisect', 'run', sys.executable, str(oracle), 'abort', ok=False)
check('bisect: oracle status 128 aborts automation', aborted.returncode != 0 and '128' in aborted.stdout + aborted.stderr)
command(history, 'bisect', 'reset')
check('bisect: final worktree is clean and main restored',
text(history, 'status', '--porcelain') == '' and text(history, 'symbolic-ref', '--short', 'HEAD') == 'main')
print(json.dumps({'version': version, 'checks_passed': len(checks), 'checks': checks,
'scope': 'Disposable local repositories and a file-transport bare remote only. '
'No network hosting, credentials, production application, branch protection, '
'server hooks or Git 2.56 runtime tested.',
'sourceSha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}, indent=2))
Após fetch, A mantém L e observa C em origin/main. O lease contra O falha; o lease contra C permite uma substituição que não inclui C.
Armadilhas comuns
Atualizar o lease sem rever o novo commit; tratar fetch como integração; generalizar pruning normal a tags; confundir remoto local com alojamento real.
Tópicos relacionados: Estados e index · Integração e backports · Diagnóstico reproduzível
Compara as referências antes e depois da operação e regista o conteúdo que a decisão pretende preservar.
Referência: Git manual and original DR local experiment evidence · Git 2.56; workflow concepts compatible with modern Git 2.x