← AZ-400: DevOps da entrega à operação
22 / 26 · 110 MIN

Repositórios: trabalho paralelo e recuperação reproduzível

Distinguir estado local, referências partilhadas e dependências necessárias para recuperar um hotfix com worktrees, bundles e submódulos.

Separar trabalho urgente sem perder o contexto

Uma correção urgente não deve obrigar a misturar o trabalho atual com o hotfix. Um worktree ligado permite abrir outra branch numa pasta própria, conservando a área original. Antes de o criar, confirma o commit de partida e dá à nova alteração uma branch identificável. O Git recusa por predefinição abrir a mesma branch em dois worktrees; ultrapassar essa proteção não cria duas branches independentes. No exercício, main contém a versão base e hotfix recebe uma alteração de service.txt. A equipa pode comparar os dois contextos sem guardar a alteração urgente na branch principal. Regista o destino e o estado de cada área para que a próxima pessoa não execute comandos no diretório errado. Isolamento de ficheiros não substitui revisão, testes ou autorização de entrega.

Conhecer o que é separado e o que é comum

Cada worktree tem o seu HEAD, índice e ficheiros de trabalho, mas os worktrees ligados pertencem ao mesmo repositório. O laboratório prepara service.txt no índice de hotfix e confirma que o índice de main mantém base. Depois cria um commit e resolve a referência hotfix a partir da área principal. Isso não significa que tenha ocorrido merge: main continua a apontar para o commit inicial. O mesmo raciocínio evita tratar uma pasta ligada como backup independente. Se a base de objetos comum ficar indisponível, a existência de duas pastas não demonstra duas cópias recuperáveis. Para uma passagem entre equipas, explica explicitamente esta relação e identifica onde estão as referências que devem ser preservadas. Usa comandos Git para consultar estado, sem editar manualmente os ficheiros internos do repositório.

Preservar estado local antes de limpar

Um commit não contém automaticamente todos os ficheiros que estão à vista. Há alterações preparadas no índice, alterações apenas no working tree e ficheiros não acompanhados. Quando worktree remove recusa uma área suja, investiga estas categorias antes de usar uma opção de força. A branch pode preservar commits e ainda deixar uma nota de investigação sem proteção. O exercício demonstra também restore --staged: retira a alteração do índice, usando HEAD por predefinição, mas mantém a edição no ficheiro. Acrescentar --worktree muda o destino da operação e pode descartar precisamente o conteúdo que se pretendia guardar. Confirma primeiro o resultado pretendido, inspeciona diferenças e preserva o que é necessário. Um comando bem sucedido não prova que o utilizador tenha escolhido a área de estado correta.

Stash e reflog têm âmbito local

Os worktrees não fornecem uma lista de stash privada por pasta. A referência refs/stash é partilhada; a entrada mais recente pode ter sido criada noutra área. Inspeciona o conteúdo e o identificador antes de aplicar uma entrada, e considera a possibilidade de conflitos no destino. O laboratório usa apply para conservar a entrada durante a observação; não demonstra que qualquer stash possa ser aplicado em qualquer branch. Já o reflog regista movimentos de referências no repositório local. Um clone novo não herda os movimentos que só existiam no portátil de outra pessoa. Se um commit importante deixou de estar referenciado, procura evidência disponível e preserva os objetos encontrados antes de ações de limpeza. Não prometas recuperação de conteúdo que nunca foi disponibilizado fora de uma máquina já inacessível.

Verificar os pré-requisitos de um bundle

Um bundle pode transportar o histórico alcançável por referências escolhidas. Um conjunto incremental exclui uma base que o destinatário tem de possuir. No laboratório, base..main verifica num repositório que já tem base e falha num repositório vazio. A diferença não demonstra corrupção: mostra que os destinos têm condições diferentes. A recuperação começa por um bundle autocontido da base e só depois aplica o incremento. Ao clonar o bundle do exercício, escolhe main explicitamente; não dependas da inferência de uma branch inicial que o pacote pode não indicar como HEAD. Regista quais as referências incluídas e quais os pré-requisitos externos ao pacote. Tamanho do ficheiro, nome da extensão e criação sem erro não substituem um ensaio no tipo de destino previsto.

Fixar e obter dependências em submódulos

Um superprojeto identifica o commit esperado de um submódulo através de um gitlink. Publicar esse apontador não equivale a disponibilizar todos os objetos do repositório dependente. No segundo exercício, a biblioteca tem uma versão aprovada A e avança depois para B; o superprojeto continua a pedir A. Um checkout normal do submódulo usa A e deixa HEAD destacado, sem isso constituir erro. Para uma árvore com dependências aninhadas, trata inicialização e atualização recursiva de forma explícita. Confirma também os acessos do utilizador ou agente que fará a recuperação. Não substituas silenciosamente um commit indisponível pelo mais recente, pois isso altera o conteúdo a construir. O laboratório usa repositórios locais separados; não combina submódulos com worktrees nem valida permissões de um servidor remoto.

Executar e interpretar o laboratório

O script apresentado cria repositórios temporários e usa apenas transporte local. Desativa hooks, assinatura e configuração global para evitar dependências da máquina do aluno; no fim remove apenas a sua pasta temporária. As verificações observam índices separados, referências partilhadas, recusa de remoção com alterações, retirada do índice sem perda da edição, stash partilhado, recuperação de bundles e checkout do submódulo fixado. Lê os nomes das verificações e associa cada resultado à afirmação que suporta. O teste de recursive não constrói uma hierarquia aninhada: essa capacidade é sustentada pela documentação e deve ser experimentada num exercício adicional se fizer parte da aceitação real. Não transformes os resultados locais numa garantia sobre latência de rede, autorização remota, políticas de hosting ou disponibilidade de armazenamento.

Decidir se o hotfix pode ser entregue

No caso fictício, RUN recebe um bundle incremental para um repositório vazio e uma nota operacional continua apenas na máquina do autor. A decisão de readiness tem duas lacunas distintas: histórico insuficiente e parâmetros por preservar e rever. Obtém uma base aprovada ou um pacote autocontido; guarda a nota por um meio autorizado e confirma a versão do procedimento. Depois reproduz a correção no ambiente de ensaio previsto, verificando identificadores e conteúdo das dependências necessárias. Uma alternativa pode cumprir o mesmo critério por outro meio, mas deve demonstrá-lo antes da intervenção. Se não existe exceção aprovada e a reprodução falha, mantém a entrega pendente. Esta regra pertence ao cenário; o curso não inventa um procedimento interno do banco nem transforma um ensaio Git em aprovação de produção.

"""Original local Git exercise. Creates and removes only its own temporary directory.
No network, credentials, user configuration, production repository or Git hosting API.
"""
import json
import os
import subprocess
import tempfile
from pathlib import Path

checks = []
def check(name, condition):
    if not condition:
        raise AssertionError(name)
    checks.append(name)

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_TERMINAL_PROMPT='0', GIT_ALLOW_PROTOCOL='file', LC_ALL='C')

with tempfile.TemporaryDirectory(prefix='dr-az400-recovery-') as directory:
    root = Path(directory)
    hooks = root / 'empty-hooks'
    hooks.mkdir()
    def git(repo, *args, ok=True):
        result = subprocess.run(['git', '-c', 'user.name=DR local exercise',
            '-c', 'user.email=exercise@example.invalid', '-c', 'commit.gpgsign=false',
            '-c', 'tag.gpgsign=false', '-c', 'core.hooksPath=' + str(hooks),
            '-c', 'gc.auto=0', '-c', 'maintenance.auto=false', '-C', str(repo), *map(str, args)],
            env=env, text=True, capture_output=True, timeout=20)
        if ok and result.returncode:
            raise AssertionError('Local Git command failed: ' + ' '.join(map(str, args)) + '\n' + result.stderr)
        return result
    def init(name):
        repo = root / name
        repo.mkdir()
        git(repo, 'init', '--template=' + str(hooks), '-b', 'main')
        return repo
    def commit(repo, filename, text, message):
        (repo / filename).write_text(text)
        git(repo, 'add', '--', filename)
        git(repo, 'commit', '-m', message)
        return git(repo, 'rev-parse', 'HEAD').stdout.strip()

    main = init('main-repo')
    base = commit(main, 'service.txt', 'base\n', 'base release')
    linked = root / 'hotfix-worktree'
    git(main, 'worktree', 'add', '-b', 'hotfix', linked, 'HEAD')
    duplicate = git(main, 'worktree', 'add', root / 'duplicate', 'main', ok=False)
    check('same checked-out branch refused by default', duplicate.returncode != 0)
    (linked / 'service.txt').write_text('hotfix draft\n')
    git(linked, 'add', 'service.txt')
    check('linked index contains draft', git(linked, 'show', ':service.txt').stdout == 'hotfix draft\n')
    check('main index remains independent', git(main, 'show', ':service.txt').stdout == 'base\n')
    check('main working tree remains independent', (main / 'service.txt').read_text() == 'base\n')
    check('main HEAD remains main', git(main, 'symbolic-ref', '--short', 'HEAD').stdout.strip() == 'main')
    check('linked HEAD is hotfix', git(linked, 'symbolic-ref', '--short', 'HEAD').stdout.strip() == 'hotfix')
    removal = git(main, 'worktree', 'remove', linked, ok=False)
    check('dirty worktree removal refused', removal.returncode != 0 and linked.exists())
    git(linked, 'restore', '--staged', '--', 'service.txt')
    check('restore staged resets only index', git(linked, 'show', ':service.txt').stdout == 'base\n')
    check('restore staged retains working file', (linked / 'service.txt').read_text() == 'hotfix draft\n')
    git(linked, 'stash', 'push', '-m', 'hotfix draft for inspection')
    stash_oid = git(linked, 'rev-parse', 'refs/stash').stdout.strip()
    check('stash ref shared across linked worktrees', git(main, 'rev-parse', 'refs/stash').stdout.strip() == stash_oid)
    git(linked, 'stash', 'apply', stash_oid)
    git(linked, 'add', 'service.txt')
    git(linked, 'commit', '-m', 'reviewable hotfix candidate')
    hotfix = git(linked, 'rev-parse', 'HEAD').stdout.strip()
    check('branch ref shared across worktrees', git(main, 'rev-parse', 'hotfix').stdout.strip() == hotfix)
    check('main branch not moved by linked commit', git(main, 'rev-parse', 'main').stdout.strip() == base)
    check('main reflog lacks linked HEAD checkout event', 'moving from' not in git(main, 'reflog', 'show', 'HEAD').stdout)

    full = root / 'base.bundle'
    git(main, 'bundle', 'create', full, 'main')
    later = commit(main, 'service.txt', 'release two\n', 'second release')
    (main / 'draft.txt').write_text('not committed\n')
    increment = root / 'increment.bundle'
    git(main, 'bundle', 'create', increment, base + '..main')
    empty = init('empty-recipient')
    check('increment needs prerequisite history', git(empty, 'bundle', 'verify', increment, ok=False).returncode != 0)
    restored = root / 'restored'
    git(root, 'clone', '--branch', 'main', full, restored)
    check('full bundle restores committed base', git(restored, 'rev-parse', 'HEAD').stdout.strip() == base)
    check('recipient with base verifies increment', git(restored, 'bundle', 'verify', increment).returncode == 0)
    git(restored, 'fetch', increment, 'refs/heads/main:refs/heads/recovered')
    check('increment restores expected commit', git(restored, 'rev-parse', 'recovered').stdout.strip() == later)
    check('increment restores expected file', git(restored, 'show', 'recovered:service.txt').stdout == 'release two\n')
    check('uncommitted file absent from bundle history', git(restored, 'cat-file', '-e', 'recovered:draft.txt', ok=False).returncode != 0)
    check('source draft remains intact', (main / 'draft.txt').read_text() == 'not committed\n')
    check('reflog entries not transported by bundle', 'second release' not in git(restored, 'reflog', 'show', 'HEAD').stdout)

    # Separate repositories: the exercise does not combine submodules with worktrees.
    library = init('local-library')
    lib_one = commit(library, 'rule.txt', 'approved rule\n', 'library one')
    parent = init('superproject')
    commit(parent, 'README.txt', 'fictional adapter\n', 'adapter base')
    git(parent, 'submodule', 'add', library, 'rules')
    git(parent, 'commit', '-m', 'pin approved rule')
    lib_two = commit(library, 'rule.txt', 'new unapproved rule\n', 'library two')
    consumer = root / 'consumer'
    git(root, 'clone', parent, consumer)
    check('ordinary parent clone does not populate submodule', not (consumer / 'rules/rule.txt').exists())
    git(consumer, 'submodule', 'update', '--init', '--recursive', '--checkout')
    check('submodule uses recorded commit, not remote head', git(consumer / 'rules', 'rev-parse', 'HEAD').stdout.strip() == lib_one)
    check('newer source commit is distinct', lib_two != lib_one)
    check('default checkout leaves submodule detached', git(consumer / 'rules', 'symbolic-ref', '-q', 'HEAD', ok=False).returncode != 0)
    check('submodule content matches approved pin', (consumer / 'rules/rule.txt').read_text() == 'approved rule\n')
    git(main, 'worktree', 'remove', linked)
    check('clean linked worktree can be removed', not linked.exists())
    check('removing linked worktree preserves branch commit', git(main, 'rev-parse', 'hotfix').stdout.strip() == hotfix)
    version = git(root, '--version').stdout.strip()

print(json.dumps({'passed': True, 'gitVersion': version, 'checks': len(checks),
                  'checkNames': checks, 'networkUsed': False,
                  'temporaryDirectoryRemoved': not Path(directory).exists(),
                  'limitations': 'Local Git only. No remote authorization, hosting policy, cloud pipeline, storage-loss simulation or cross-version execution.'}, indent=2))
NA PRÁTICA

Um bundle base..hotfix chega a RUN sem o histórico base e sem uma nota de parâmetros não acompanhada por Git. Completar o pacote e ensaiar a recuperação são condições distintas de criar o ficheiro com sucesso.

Armadilhas comuns

Tratar worktrees como clones independentes, stash como privado da pasta, bundle incremental como backup completo e gitlink como transporte do repositório dependente.

Tópicos relacionados: Revisão e backport de hotfixes · Backup e recuperação de dependências · Agentes de build limpos

Leva esta ideia contigo

Recuperação exige conteúdo e pré-requisitos disponíveis no destino. Confirma o estado que cada comando altera e demonstra a reconstrução da versão pretendida.

Criar conta

Referência: git-worktree manual · AZ-400 objectives 2026-07-27

Microsoft é uma marca comercial do grupo de empresas Microsoft. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Microsoft. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.