guide

Precedência do gitignore e padrões de negação (!)

Por que a última regra vence, a precedência entre .gitignore, info/exclude e a configuração global, e por que ! não reinclui arquivos quando o diretório pai foi excluído.

Quando uma regra do gitignore não se comporta como esperado, a causa quase sempre está na ordem e no diretório pai. Este texto resume qual regra o git segue quando várias se aplicam.

No mesmo arquivo: vence a última regra correspondente

Dentro de um .gitignore, o git lê de cima para baixo e o último padrão correspondente decide o resultado. Se esse padrão começa com !, o arquivo não é ignorado; caso contrário, é ignorado.

*.log
!important.log

important.log corresponde às duas linhas, mas a última é uma negação, então ele é rastreado. Inverta a ordem e o resultado também se inverte.

!important.log
*.log

Agora a última correspondência de important.log é *.log, então ele é ignorado. É por isso que, ao juntar vários modelos no gerador, a ordem de seleção vira a ordem das regras. Por exemplo, ao usar *.jar do modelo Java com !gradle-wrapper.jar do modelo Gradle, o Gradle precisa vir depois.

Precedência entre arquivos

O git lê regras de quatro lugares. Quanto mais acima na lista, maior a precedência.

  1. Padrões recebidos na linha de comando (alguns comandos, como git ls-files --exclude)
  2. .gitignore no mesmo diretório do caminho ou em diretórios pais — arquivos mais profundos sobrepõem os de cima
  3. $GIT_DIR/info/exclude (normalmente .git/info/exclude)
  4. O arquivo apontado pela configuração core.excludesFile (gitignore global)

Por exemplo, se o .gitignore da raiz tem *.tmp e sub/.gitignore tem !*.tmp, então sub/y.tmp é rastreado pela regra de negação do arquivo mais profundo. Já escrever !x em .git/info/exclude não adianta: o x do .gitignore tem precedência e x continua ignorado.

$ git check-ignore -v x y.tmp sub/y.tmp
.gitignore:1:x	x
.gitignore:2:*.tmp	y.tmp
sub/.gitignore:1:!*.tmp	sub/y.tmp

A exceção mais importante: diretório pai excluído não permite reinclusão

A documentação do git diz: não é possível reincluir um arquivo se um diretório pai dele estiver excluído. Por desempenho, o git nem entra em diretórios excluídos, então regras ! para arquivos lá dentro nem chegam a ser lidas.

logs/
!logs/keep.txt

Com estas regras, logs/keep.txt continua ignorado. Ao executar git check-ignore -v, a regra decisiva aparece como logs/.

A solução é excluir o conteúdo do diretório, e não o diretório.

logs/*
!logs/keep.txt

logs/* não corresponde ao próprio diretório logs, então o git entra nele e aplica a negação a keep.txt. O /log/* + !/log/.keep do modelo Rails e o .vscode/* + !.vscode/settings.json do modelo VS Code usam esse mesmo princípio.

Para reincluir um caminho mais profundo, é preciso reincluir cada diretório intermediário.

config/*
!config/app/
config/app/*
!config/app/defaults.yml

Padrão de lista de permissões (allowlist)

O mesmo princípio serve quando você quer listar só o que deve ser rastreado em vez do que deve ser ignorado.

# ignora tudo na raiz
/*
# reinclui só o necessário
!/.gitignore
!/src/
!/README.md

/* corresponde a cada item da raiz, mas não exclui a própria raiz, então !/src/ funciona. Nesse esquema o próprio .gitignore também cai em /*, então não esqueça !/.gitignore. De fato, sem essa linha, git status --ignored mostra o .gitignore na lista de ignorados.

Regras duplicadas e ordem

Se a mesma regra aparece várias vezes, as ocorrências anteriores não afetam o resultado, porque a mesma regra mais abaixo sempre corresponde depois. Já remover uma duplicata posterior pode mudar o resultado se houver uma negação no meio.

*.log
!debug.log
*.log

Aqui, remover o último *.log faz debug.log passar a ser rastreado. O gerador deste site detecta esse caso e só remove duplicatas posteriores quando não há regra de negação entre elas, mantendo as demais.

Referências

← AnteriorGuia completo da sintaxe de padrões do .gitignore