O que Linus Torvalds quer dizer quando diz que o Git "nunca" controla um arquivo?

283

Citando Linus Torvalds, quando perguntado quantos arquivos o Git pode manipular durante sua Tech Talk no Google em 2007 (43:09):

… O Git rastreia seu conteúdo. Ele nunca rastreia um único arquivo. Você não pode rastrear um arquivo no Git. O que você pode fazer é acompanhar um projeto que possui um único arquivo, mas se o seu projeto tiver um único arquivo, faça-o e você poderá fazê-lo, mas se acompanhar 10.000 arquivos, o Git nunca os verá como arquivos individuais. O Git pensa em tudo como conteúdo completo. Toda a história no Git é baseada na história de todo o projeto ...

(Transcrições aqui .)

No entanto, quando você mergulhar o livro Git , a primeira coisa que é dito é que um arquivo no Git pode ser tanto rastreado ou untracked . Além disso, parece-me que toda a experiência do Git é voltada para o versionamento de arquivos. Quando o uso git diffou a git statussaída são apresentados por arquivo. Ao usar, git addvocê também escolhe por arquivo. Você pode até revisar o histórico com base em arquivos e é extremamente rápido.

Como essa declaração deve ser interpretada? Em termos de rastreamento de arquivos, como o Git é diferente de outros sistemas de controle de origem, como o CVS?

Simón Ramírez Amaya
fonte
20
reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git - "Para onde você está no momento, eu suspeito que o mais importante é perceber que há uma diferença entre como o Git apresenta arquivos para os usuários e como ele os lida internamente . Conforme apresentado ao usuário, um instantâneo contém arquivos completos, não apenas diffs. Mas internamente, sim, o Git usa diffs para gerar arquivos de pacote que armazenam revisões com eficiência ". (Este é nítido contraste com, por exemplo, Subversion..)
user2864740
5
O Git não rastreia arquivos, rastreia conjuntos de alterações . A maioria dos sistemas de controle de versão rastreia arquivos. Como um exemplo de como / por que isso pode importar, tente fazer check-in em um diretório vazio para o git (spolier: você não pode, porque esse é um conjunto de alterações "vazio").
Elliott Frisch
12
@ ElliottFrisch Isso não parece certo. Sua descrição está mais próxima do que, por exemplo, darcs . O Git armazena instantâneos, não conjuntos de alterações.
melpomene
4
Eu acho que ele quer dizer que o Git não rastreia um arquivo diretamente. Um arquivo inclui seu nome e conteúdo. O Git rastreia o conteúdo como blobs. Dado apenas um blob, você não pode dizer qual é o nome do arquivo correspondente. Pode ser o conteúdo de vários arquivos com nomes diferentes em caminhos diferentes. As ligações entre um nome de caminho e um blob são descritas em um objeto de árvore.
ElpieKay 10/04/19
3
Relacionado: Seguimento de Randal Schwartz à palestra de Linus (também uma palestra do Google Tech) - "... O que é realmente o Git ... Linus disse o que o Git NÃO é".
Peter Mortensen

Respostas:

316

No CVS, o histórico foi rastreado por arquivo. Uma ramificação pode consistir em vários arquivos com suas próprias revisões, cada um com seu próprio número de versão. O CVS foi baseado no RCS ( Revision Control System ), que rastreia arquivos individuais de maneira semelhante.

Por outro lado, o Git tira instantâneos do estado de todo o projeto. Os arquivos não são rastreados e versionados independentemente; uma revisão no repositório refere-se a um estado de todo o projeto, não a um arquivo.

Quando o Git se refere ao rastreamento de um arquivo, significa simplesmente que ele deve ser incluído no histórico do projeto. A palestra de Linus não se referia ao rastreamento de arquivos no contexto do Git, mas estava contrastando o modelo CVS e RCS com o modelo baseado em instantâneo usado no Git.

bk2204
fonte
4
Você pode acrescentar que é por isso que no CVS e no Subversion, você pode usar tags como $Id$em um arquivo. O mesmo não funciona no git, porque o design é diferente.
gerrit
58
E o conteúdo não está vinculado a um arquivo como seria de esperar. Tente mover 80% do código de um arquivo para outro. O Git detecta automaticamente a movimentação de um arquivo + 20% de alteração, mesmo quando você acabou de mover o código nos arquivos existentes.
allo
13
@allo Como efeito colateral disso, o git pode fazer uma coisa que os outros não: quando dois arquivos são mesclados e você usa "git blame -C", o git pode procurar nos dois históricos. No rastreamento baseado em arquivo, você precisa escolher qual dos arquivos originais é o original real e todas as outras linhas parecem totalmente novas.
Izkata
1
@allo, Izkata - E é a entidade consultora que resolve tudo isso analisando o conteúdo do repositório no momento da consulta (confirmar históricos e diferenças entre árvores e blobs referenciados), em vez de exigir que a entidade comprometedora e seu usuário humano especifiquem ou sintetizem corretamente essas informações no momento da confirmação - nem o desenvolvedor da ferramenta de recompra para projetar e implementar esse recurso e o esquema de metadados correspondente antes da implantação da ferramenta. Torvalds argumentou que essa análise só melhorará ao longo do tempo, e todo o histórico de todos os repositórios Git desde o primeiro dia será beneficiado.
Jeremy
1
@allo Sim, e para enfatizar o fato de que o git não funciona no nível de um arquivo, você nem precisa confirmar todas as alterações em um arquivo de uma só vez; você pode confirmar intervalos de linhas arbitrários enquanto deixa outras alterações no arquivo fora da confirmação. É claro que a interface do usuário não é tão simples assim, a maioria não faz isso, mas raramente tem seus usos.
Alvin Thompson
103

Eu concordo com brian m. resposta de carlson : Linus realmente faz distinção, pelo menos em parte, entre sistemas de controle de versão orientados a arquivos e comprometidos. Mas acho que há mais do que isso.

No meu livro , que está parado e pode nunca terminar, tentei criar uma taxonomia para os sistemas de controle de versão. Na minha taxonomia, o termo para o que estamos interessados ​​aqui é a atomicidade do sistema de controle de versão. Veja o que está atualmente na página 22. Quando um VCS possui atomicidade no nível de arquivo, existe de fato um histórico para cada arquivo. O VCS deve lembrar o nome do arquivo e o que ocorreu em cada ponto.

Git não faz isso. O Git tem apenas um histórico de confirmações - a confirmação é sua unidade de atomicidade e a história é o conjunto de confirmações no repositório. O que uma confirmação lembra são os dados - uma árvore inteira cheia de nomes de arquivos e o conteúdo que acompanha cada um desses arquivos - além de alguns metadados: por exemplo, quem fez a confirmação, quando, por que e o ID de hash interno do Git do commit pai do commit. (É esse pai, e o gráfico de ciclismo direcionado formado pela leitura de todos os commits e seus pais, que é o histórico em um repositório.)

Observe que um VCS pode ser orientado a confirmação, mas ainda assim armazenar dados arquivo por arquivo. Esse é um detalhe de implementação, embora às vezes seja importante, e o Git também não faz isso. Em vez disso, cada confirmação registra uma árvore , com o objeto da árvore que codifica os nomes dos arquivos , modos (isto é, este arquivo é executável ou não?) E um ponteiro para o conteúdo real do arquivo . O conteúdo em si é armazenado independentemente, em um objeto de blob . Como um objeto de confirmação, um blob obtém um ID de hash exclusivo para seu conteúdo - mas, diferentemente de um commit, que pode aparecer apenas uma vez, o blob pode aparecer em muitos commits. Portanto, o conteúdo do arquivo subjacente no Git é armazenado diretamente como um blob e, indiretamente, em um objeto de árvore cujo hash ID é registrado (direta ou indiretamente) no objeto de confirmação.

Quando você pede ao Git para mostrar o histórico de um arquivo usando:

git log [--follow] [starting-point] [--] path/to/file

o que o Git está realmente fazendo é percorrer o histórico de consolidação , que é o único histórico que o Git possui, mas não mostra nenhum desses consertos, a menos que:

  • o commit é um commit sem mesclagem e
  • o pai desse commit também possui o arquivo, mas o conteúdo do pai é diferente ou o pai do commit não tem o arquivo

(mas algumas dessas condições podem ser modificadas por meio de git logopções adicionais , e é muito difícil descrever o efeito colateral chamado Simplificação do Histórico, que faz com que o Git omita alguns commits do histórico. O histórico do arquivo que você vê aqui não existe exatamente no repositório, em certo sentido: em vez disso, é apenas um subconjunto sintético do histórico real. Você obterá um "histórico de arquivos" diferente se usar git logopções diferentes !

torek
fonte
Outra coisa a acrescentar é que isso permite que o Git faça coisas como clones rasos. Ele só precisa recuperar o commit da cabeça e todos os blobs a que se refere. Não é necessário recriar arquivos aplicando conjuntos de alterações.
Wes Toleman
@ WesToleman: definitivamente torna isso mais fácil. O Mercurial armazena deltas, com redefinições ocasionais, e embora o pessoal do Mercurial pretenda adicionar clones rasos (o que é possível devido à idéia de "redefinir"), eles ainda não o fizeram (porque é mais um desafio técnico).
Torek #
@torek Tenho uma dúvida a respeito de sua descrição sobre Git respondendo a um pedido histórico do arquivo, mas eu acho que merece a sua própria pergunta adequada: stackoverflow.com/questions/55616349/...
Simón Ramírez Amaya
@torek Obrigado pelo link do seu livro, não vi mais nada parecido.
gnarledRoot
17

A parte confusa está aqui:

O Git nunca os vê como arquivos individuais. O Git pensa em tudo como conteúdo completo.

O Git geralmente usa hashes de 160 bits no lugar de objetos em seu próprio repositório. Uma árvore de arquivos é basicamente uma lista de nomes e hashes associados ao conteúdo de cada um (mais alguns metadados).

Mas o hash de 160 bits identifica exclusivamente o conteúdo (dentro do universo do banco de dados git). Portanto, uma árvore com hashes como conteúdo inclui o conteúdo em seu estado.

Se você alterar o estado do conteúdo de um arquivo, seu hash será alterado. Mas se o hash for alterado, o hash associado ao conteúdo do nome do arquivo também será alterado. O que, por sua vez, altera o hash da "árvore de diretórios".

Quando um banco de dados git armazena uma árvore de diretórios, essa árvore de diretórios implica e inclui todo o conteúdo de todos os subdiretórios e todos os arquivos nele .

Ele é organizado em uma estrutura de árvore com ponteiros (imutáveis, reutilizáveis) para blobs ou outras árvores, mas logicamente é um instantâneo único de todo o conteúdo de toda a árvore. A representação no banco de dados git não é o conteúdo de dados simples, mas logicamente são todos os dados e nada mais.

Se você serializou a árvore em um sistema de arquivos, excluiu todas as pastas .git e disse ao git para adicionar a árvore novamente ao banco de dados, você acabaria adicionando nada ao banco de dados - o elemento já estaria lá.

Pode ajudar pensar nos hashes do git como um ponteiro de referência contado para dados imutáveis.

Se você criou um aplicativo em torno disso, um documento é um monte de páginas, com camadas, grupos e objetos.

Quando você deseja alterar um objeto, é necessário criar um grupo completamente novo para ele. Se você deseja alterar um grupo, é necessário criar uma nova camada, que precisa de uma nova página, que precisa de um novo documento.

Toda vez que você altera um único objeto, ele gera um novo documento. O documento antigo continua a existir. Os documentos novos e antigos compartilham a maior parte de seu conteúdo - eles têm as mesmas páginas (exceto 1). Essa página tem as mesmas camadas (exceto 1). Essa camada tem os mesmos grupos (exceto 1). Esse grupo tem os mesmos objetos (exceto 1).

E, do mesmo modo, quero dizer logicamente uma cópia, mas em termos de implementação é apenas mais um ponteiro de referência contado para o mesmo objeto imutável.

Um repositório Git é muito parecido com isso.

Isso significa que um dado conjunto de alterações git contém sua mensagem de confirmação (como um código hash), sua árvore de trabalho e suas alterações pai.

Essas alterações pai contêm as alterações pai, desde o início.

A parte do repositório git que contém a história é essa cadeia de mudanças. Essa cadeia de alterações a um nível acima da árvore "diretório" - de uma árvore "diretório", você não pode acessar exclusivamente um conjunto de alterações e a cadeia de alterações.

Para descobrir o que acontece com um arquivo, você começa com esse arquivo em um conjunto de alterações. Esse changeset tem um histórico. Freqüentemente nesse histórico, existe o mesmo arquivo nomeado, às vezes com o mesmo conteúdo. Se o conteúdo for o mesmo, não houve alteração no arquivo. Se for diferente, há uma mudança e é preciso trabalhar para descobrir exatamente o que.

Às vezes o arquivo se foi; mas a árvore "diretório" pode ter outro arquivo com o mesmo conteúdo (o mesmo código de hash), para que possamos acompanhá-lo dessa maneira (observe; é por isso que você deseja um commit para mover um arquivo separado de um commit para -editar). Ou o mesmo nome de arquivo e, após verificar o arquivo, é semelhante o suficiente.

Assim, o git pode juntar um "histórico de arquivos".

Mas esse histórico de arquivo vem da análise eficiente do "conjunto de alterações inteiro", não de um link de uma versão do arquivo para outra.

Yakk - Adam Nevraumont
fonte
12

"git não rastreia arquivos" basicamente significa que as confirmações do git consistem em um instantâneo da árvore de arquivos que conecta um caminho na árvore a um "blob" e um gráfico de confirmação acompanhando o histórico das confirmações . Todo o resto é reconstruído on-the-fly por comandos como "git log" e "git blame". Essa reconstrução pode ser explicada por várias opções com que dificuldade deve parecer as alterações baseadas em arquivos. As heurísticas padrão podem determinar quando um blob muda de lugar na árvore de arquivos sem alterações ou quando um arquivo está associado a um blob diferente do que antes. Os mecanismos de compactação que o Git usa não se importam muito com os limites de blob / arquivo. Se o conteúdo já estiver em algum lugar, isso manterá o crescimento do repositório pequeno sem associar os vários blobs.

Agora esse é o repositório. O Git também possui uma árvore de trabalho e nessa árvore de trabalho há arquivos rastreados e não rastreados. Somente os arquivos rastreados são gravados no índice (área de armazenamento temporário? Cache?) E apenas o que é rastreado lá entra no repositório.

O índice é orientado a arquivos e existem alguns comandos orientados a arquivos para manipulá-lo. Mas o que acaba no repositório é apenas confirmado na forma de instantâneos da árvore de arquivos e os dados de blob associados e os ancestrais do commit.

Como o Git não rastreia históricos de arquivos e renomeia e sua eficiência não depende deles, às vezes você precisa tentar algumas vezes com opções diferentes até que o Git produza o histórico / diferenças / acusações de seu interesse para históricos não triviais.

Isso é diferente com sistemas como o Subversion, que registram e não reconstroem histórias. Se não estiver registrado, você não ouvirá sobre isso.

Na verdade, eu construí um instalador diferencial que comparou as árvores de lançamento, verificando-as no Git e produzindo um script duplicando seus efeitos. Como algumas vezes árvores inteiras foram movidas, isso produziu instaladores diferenciais muito menores do que substituir / excluir tudo o que teria produzido.


fonte
7

O Git não rastreia um arquivo diretamente, mas rastreia os instantâneos do repositório, e esses instantâneos consistem em arquivos.

Aqui está uma maneira de ver isso.

Em outros sistemas de controle de versão (SVN, Rational ClearCase), é possível clicar com o botão direito do mouse em um arquivo e obter seu histórico de alterações .

No Git, não há comando direto que faça isso. Veja esta pergunta . Você ficará surpreso com quantas respostas diferentes existem. Não existe uma resposta simples porque o Git não controla simplesmente um arquivo , não da maneira que o SVN ou o ClearCase o faz.

Visão dupla Stout Fat Heavy
fonte
5
Acho que entendi o que você está tentando dizer, mas "No Git, não há comando direto que faça isso" é diretamente contradito pelas respostas à pergunta à qual você está vinculado. Embora seja verdade que a versão acontece no nível de todo o repositório, normalmente existem várias maneiras de conseguir algo no Git, portanto, ter vários comandos para mostrar o histórico de um arquivo não é uma evidência de muita coisa.
Joe Lee-Moyet
Analisei as primeiras respostas da pergunta que você vinculou e todas elas usam git logou algum programa construído sobre isso (ou algum apelido que faz a mesma coisa). Mas mesmo que houvesse muitas maneiras diferentes, como Joe diz, isso também é válido para mostrar o histórico das filiais. (também git log -p <file>é embutido e faz exatamente isso) #
1010 Voo
Tem certeza de que o SVN armazena internamente alterações por arquivo? Ainda não o uso há algum tempo, mas me lembro vagamente de ter arquivos nomeados como IDs de versão, em vez de refletir a estrutura de arquivos do projeto.
Artur Biesiadowski
3

Rastrear "conteúdo", aliás, é o que levou a não rastrear diretórios vazios.
É por isso que, se você der o último arquivo de uma pasta, a própria pasta será excluída .

Esse nem sempre foi o caso, e apenas o Git 1.4 (maio de 2006) aplicou a política de "rastreamento de conteúdo" com o commit 443f833 :

status do git: pule diretórios vazios e adicione -u para mostrar todos os arquivos não rastreados

Por padrão, usamos --others --directorypara mostrar diretórios desinteressantes (para chamar a atenção do usuário) sem seu conteúdo (para organizar a saída).
Mostrar diretórios vazios não faz sentido; portanto, passe --no-empty-directoryquando o fizermos.

Dar -u(ou --untracked) desativa essa organização para permitir que o usuário obtenha todos os arquivos não rastreados.

Isso foi ecoado anos depois, em janeiro de 2011, com o commit 8fe533 , Git v1.7.4:

Isso está de acordo com a filosofia geral da interface do usuário: o git rastreia o conteúdo, não os diretórios vazios.

Enquanto isso, com o Git 1.4.3 (setembro de 2006), o Git começa a limitar o conteúdo não rastreado a pastas não vazias, com o commit 2074cb0 :

não deve listar o conteúdo de diretórios completamente não rastreados, mas apenas o nome desse diretório (mais um ' /' à direita ).

O rastreamento de conteúdo é o que permitiu ao git culpar, desde o início (Git 1.4.4, outubro de 2006, confirmar cee7f24 ) ter mais desempenho:

Mais importante, sua estrutura interna foi projetada para suportar o movimento de conteúdo (também conhecido como recortar e colar) mais facilmente, permitindo que mais de um caminho seja percorrido no mesmo commit.

Isso (conteúdo de rastreamento) também é o que colocou o git add na API do Git, com o Git 1.5.0 (dezembro de 2006, confirmar 366bfcb )

faça do 'git add' uma interface amigável de primeira classe para o índice

Isso traz o poder do índice antecipadamente, usando um modelo mental adequado, sem falar sobre o índice.
Veja, por exemplo, como toda a discussão técnica foi evacuada na página de manual do git-add.

Qualquer conteúdo a ser confirmado deve ser adicionado.
Se esse conteúdo vem de novos arquivos ou arquivos modificados, não importa.
Você só precisa "adicioná-lo" com o git-add ou fornecendo o git-commit -a(para arquivos já conhecidos, é claro).

Isso é o que tornou git add --interactivepossível, com o mesmo Git 1.5.0 ( commit 5cde71d )

Após fazer a seleção, responda com uma linha vazia para preparar o conteúdo dos arquivos da árvore de trabalho para os caminhos selecionados no índice.

É também por isso que, para remover recursivamente todo o conteúdo de um diretório, você precisa passar a -ropção, não apenas o nome do diretório como o <path>(ainda Git 1.5.0, confirmar 9f95069 ).

Ver o conteúdo do arquivo em vez do próprio arquivo é o que permite mesclar cenários como o descrito em commit 1de70db (Git v2.18.0-rc0, abr. 2018)

Considere a mesclagem a seguir com um conflito de renomeação / adição:

  • lado A: modificar foo, adicionar alheiosbar
  • lado B: renomear foo->bar(mas não modifique o modo ou o conteúdo)

Nesse caso, a combinação de três vias de foo original, fo de A e B barresultará em um nome de caminho desejado barcom o mesmo modo / conteúdo que A tinha para foo.
Assim, A tinha o modo e o conteúdo corretos para o arquivo e o nome do caminho correto presente (a saber, bar).

O commit 37b65ce , Git v2.21.0-rc0, dezembro de 2018, melhorou recentemente as resoluções de conflito em colisão.
E commit bbafc9c firther ilustra a importância de considerar o conteúdo do arquivo , melhorando a manipulação de conflitos de renomear / renomear (2to1):

  • Em vez de armazenar arquivos em collide_path~HEADe collide_path~MERGE, os arquivos são mesclados nos dois sentidos e registrados em collide_path.
  • Em vez de gravar a versão do arquivo renomeado que existia no lado renomeado no índice (ignorando, portanto, as alterações feitas no arquivo no lado do histórico sem a renomeação), fazemos uma mesclagem de conteúdo de três vias na renomeada caminho, armazene-o no estágio 2 ou 3.
  • Observe que, como a mesclagem de conteúdo para cada renomeação pode ter conflitos e, como precisamos mesclar os dois arquivos renomeados, podemos terminar com marcadores de conflito aninhados.
VonC
fonte