Cosa significa Linus Torvalds quando dice che Git "mai e poi mai" tiene traccia di un file?


284

Citando Linus Torvalds quando gli viene chiesto quanti file Git può gestire durante il suo Tech Talk su Google nel 2007 (43:09):

... Git tiene traccia dei tuoi contenuti. Non tiene mai traccia di un singolo file. Non è possibile tenere traccia di un file in Git. Quello che puoi fare è che puoi tenere traccia di un progetto che ha un singolo file, ma se il tuo progetto ha un singolo file, assicurati di farlo e puoi farlo, ma se segui 10.000 file, Git non li vede mai come singoli file. Git considera tutto come il contenuto completo. Tutta la storia di Git si basa sulla storia dell'intero progetto ...

(Trascrizioni qui .)

Eppure, quando ci si immerge nel libro Git , la prima cosa che ti viene detto è che un file in Git può essere rintracciato o non tracciata . Inoltre, mi sembra che l'intera esperienza di Git sia orientata al controllo delle versioni dei file. Quando si utilizza git diffo l' git statusoutput viene presentato in base al file. Quando lo usi git addpuoi anche scegliere in base al file. Puoi persino rivedere la cronologia su base file ed è velocissimo.

Come dovrebbe essere interpretata questa affermazione? In termini di tracciamento dei file, in che cosa differisce Git dagli altri sistemi di controllo del codice sorgente, come CVS?


20
reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git - "Per dove siete in questo momento, ho il sospetto che cosa c'è di più importante da capire è che c'è una differenza tra come file presenta Git per gli utenti e come si tratta con loro internamente . Come presentato all'utente, un'istantanea contiene file completi, non solo diff. Ma internamente sì, Git usa diff per generare file pack che memorizzano in modo efficiente le revisioni. " (Questo è un netto contrasto, ad esempio Subversion.)
user2864740

5
Git non tiene traccia dei file, tiene traccia dei changeset . La maggior parte dei sistemi di controllo della versione tiene traccia dei file. Come esempio di come / perché questo può importare, prova a controllare in una directory vuota per git (spolier: non puoi, perché è un changeset "vuoto").
Elliott Frisch,

12
@ElliottFrisch Non suona bene. La tua descrizione è più vicina a ciò che fa ad esempio Darcs . Git memorizza le istantanee, non i changeset.
melpomene,

4
Penso che significhi che Git non tiene traccia di un file direttamente. Un file include il suo nome e contenuto. Git tiene traccia dei contenuti come BLOB. Dato solo un BLOB, non è possibile stabilire quale sia il nome file corrispondente. Potrebbe essere il contenuto di più file con nomi diversi in percorsi diversi. I collegamenti tra un nome percorso e un BLOB sono descritti in un oggetto albero.
ElpieKay,

3
Correlati: il seguito di Randal Schwartz al discorso di Linus (anche un discorso di Google Tech) - "... Di cosa si tratta davvero Git ... Linus ha detto che NON è Git".
Peter Mortensen,

Risposte:


316

In CVS, la cronologia è stata tracciata in base al file. Un ramo potrebbe essere costituito da vari file con le proprie varie revisioni, ognuna con il proprio numero di versione. CVS si basava su RCS ( Revision Control System ), che monitorava i singoli file in modo simile.

D'altra parte, Git prende istantanee dello stato dell'intero progetto. I file non vengono tracciati e aggiornati in modo indipendente; una revisione nel repository si riferisce a uno stato dell'intero progetto, non a un file.

Quando Git si riferisce al tracciamento di un file, significa semplicemente che deve essere incluso nella cronologia del progetto. Il discorso di Linus non si riferiva al tracciamento dei file nel contesto di Git, ma stava contrastando il modello CVS e RCS con il modello basato su istantanea usato in Git.


4
Puoi aggiungere che questo è il motivo per cui in CVS e Subversion puoi usare i tag come $Id$in un file. Lo stesso non funziona in git, perché il design è diverso.
Gerrit,

58
E il contenuto non è associato a un file come ci si aspetterebbe. Prova a spostare l'80% del codice di un file in un altro. Git rileva automaticamente uno spostamento del file + una variazione del 20%, anche quando hai appena spostato il codice nei file esistenti.
allo

13
@allo Come effetto collaterale di ciò, git può fare una cosa che gli altri non possono fare: quando due file vengono uniti e si usa "git blame -C", git può guardare entrambe le storie. Nel tracciamento basato su file, devi selezionare quale dei file originali è il vero originale e le altre righe appaiono tutte nuove di zecca.
Izkata,

1
@allo, Izkata - Ed è l' entità di query che risolve tutto questo analizzando il contenuto del repository al momento della query (commit delle cronologie e delle differenze tra alberi e BLOB di riferimento), piuttosto che richiedere all'entità committente e al suo utente umano di specificare o sintetizzare correttamente queste informazioni al momento del commit, né lo sviluppatore dello strumento repository per progettare e implementare questa funzionalità e lo schema di metadati corrispondente prima della distribuzione dello strumento. Torvalds ha sostenuto che tale analisi migliorerà solo nel tempo e tutta la storia di ogni repository git dal primo giorno ne trarrà beneficio.
Jeremy,

1
@allo Yep, e per capire a casa il fatto che git non funziona a livello di file, non è nemmeno necessario eseguire il commit di tutte le modifiche in un file contemporaneamente; è possibile eseguire il commit di intervalli di righe arbitrari lasciando altre modifiche nel file al di fuori del commit. Naturalmente l'interfaccia utente non è così semplice, quindi la maggior parte non lo fa, ma raramente ha i suoi usi.
Alvin Thompson,

103

Sono d'accordo con brian m. La risposta di Carlson : Linus sta davvero distinguendo, almeno in parte, tra sistemi di controllo della versione orientati ai file e orientati al commit. Ma penso che ci sia molto di più.

Nel mio libro , che è bloccato e potrebbe non finire mai, ho cercato di trovare una tassonomia per i sistemi di controllo della versione. Nella mia tassonomia il termine per ciò che ci interessa qui è l' atomicità del sistema di controllo della versione. Guarda cosa si trova attualmente a pagina 22. Quando un VCS ha atomicità a livello di file, in effetti esiste una cronologia per ogni file. Il VCS deve ricordare il nome del file e cosa gli è successo in ogni punto.

Git non lo fa. Git ha solo una storia di commit: il commit è la sua unità di atomicità e la storia è l'insieme di commit nel repository. Ciò che un commit ricorda sono i dati - un intero albero pieno di nomi di file e contenuti associati a ciascuno di quei file - oltre ad alcuni metadati: ad esempio, chi ha effettuato il commit, quando e perché e l'ID hash Git interno del commit principale del commit. (È questo genitore, e il grafico del ciclismo diretto formato leggendo tutti i commit e i loro genitori, che è la storia in un repository.)

Si noti che un VCS può essere orientato al commit, ma può comunque archiviare i dati file per file. Questo è un dettaglio di implementazione, anche se a volte importante, e neanche Git lo fa. Al contrario, ogni commit registra un albero , con l'oggetto albero che codifica i nomi dei file , le modalità (ovvero, questo file è eseguibile o no?) E un puntatore al contenuto del file effettivo . Il contenuto stesso viene archiviato in modo indipendente, in un oggetto BLOB . Come un oggetto commit, un BLOB ottiene un ID hash univoco per il suo contenuto, ma a differenza di un commit, che può apparire solo una volta, il BLOB può apparire in molti commit. Quindi il contenuto del file sottostante in Git viene archiviato direttamente come BLOB e quindi indirettamente in un oggetto ad albero il cui ID hash è registrato (direttamente o indirettamente) nell'oggetto commit.

Quando chiedi a Git di mostrarti la cronologia di un file usando:

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

ciò che Git sta realmente facendo è percorrere la cronologia dei commit , che è l'unica storia di Git, ma non mostrarti nessuno di questi commit a meno che:

  • il commit è un commit non-merge e
  • anche il genitore di quel commit ha il file, ma il contenuto nel genitore differisce o il genitore del commit non ha affatto il file

(ma alcune di queste condizioni possono essere modificate tramite git logopzioni aggiuntive e c'è un effetto molto difficile da descrivere chiamato History Simplification che rende Git omettere del tutto alcuni commit dalla storia). La cronologia dei file che vedi qui non esiste esattamente nel repository, in un certo senso: è solo un sottoinsieme sintetico della storia reale. Otterrai una "cronologia file" diversa se utilizzi git logopzioni diverse !


Un'altra cosa da aggiungere è che consente a Git di fare cose come i cloni superficiali. Deve solo recuperare il commit della testa e tutti i BLOB a cui fa riferimento. Non è necessario ricreare i file applicando i set di modifiche.
Wes Toleman,

@WesToleman: lo rende decisamente più facile. Mercurial immagazzina delta, con ripristini occasionali, e mentre la gente di Mercurial intende aggiungere lì cloni superficiali (il che è possibile a causa dell'idea di "reset"), in realtà non l'hanno ancora fatto (perché è più una sfida tecnica).
Torek,

@torek Ho dei dubbi sulla tua descrizione di Git che risponde a una richiesta di cronologia dei file, ma penso che meriti una domanda propria: stackoverflow.com/questions/55616349/…
Simón Ramírez Amaya,

@torek Grazie per il link al tuo libro, non ho visto nient'altro come questo.
gnarledRoot

17

La parte confusa è qui:

Git non li vede mai come singoli file. Git considera tutto come il contenuto completo.

Git usa spesso hash a 160 bit al posto degli oggetti nel proprio repository. Un albero di file è fondamentalmente un elenco di nomi e hash associati al contenuto di ciascuno (oltre ad alcuni metadati).

Ma l'hash a 160 bit identifica in modo univoco il contenuto (all'interno dell'universo del database git). Quindi un albero con hash come contenuto include il contenuto nel suo stato.

Se si modifica lo stato del contenuto di un file, il relativo hash cambia. Ma se cambia l'hash, cambia anche l'hash associato al contenuto del nome del file. Che a sua volta cambia l'hash dell '"albero delle directory".

Quando un database git memorizza un albero di directory, tale albero di directory implica e include tutto il contenuto di tutte le sottodirectory e tutti i file in essa contenuti .

È organizzato in una struttura ad albero con puntatori (immutabili, riutilizzabili) a chiazze o altri alberi, ma logicamente è una singola istantanea dell'intero contenuto dell'intero albero. La rappresentazione nel database git non è il contenuto dei dati flat, ma logicamente sono tutti i suoi dati e nient'altro.

Se si serializzava l'albero in un filesystem, si cancellavano tutte le cartelle .git e si dicesse a git di aggiungere nuovamente l'albero nel suo database, si finirebbe per aggiungere nulla al database: l'elemento sarebbe già lì.

Potrebbe essere utile pensare agli hash di Git come un puntatore contato di riferimento a dati immutabili.

Se hai creato un'applicazione attorno a questo, un documento è un insieme di pagine, che hanno livelli, che hanno gruppi, che hanno oggetti.

Quando vuoi cambiare un oggetto, devi creare un gruppo completamente nuovo per esso. Se vuoi cambiare un gruppo, devi creare un nuovo livello, che ha bisogno di una nuova pagina, che ha bisogno di un nuovo documento.

Ogni volta che cambi un singolo oggetto, genera un nuovo documento. Il vecchio documento continua ad esistere. Il nuovo e vecchio documento condividono la maggior parte dei loro contenuti: hanno le stesse pagine (tranne 1). Quella pagina ha gli stessi livelli (tranne 1). Quel layer ha gli stessi gruppi (tranne 1). Quel gruppo ha gli stessi oggetti (tranne 1).

E allo stesso modo, intendo logicamente una copia, ma dal punto di vista dell'implementazione è solo un altro riferimento al puntatore dello stesso oggetto immutabile.

Un repository git è molto simile.

Ciò significa che un determinato changeset git contiene il suo messaggio di commit (come codice hash), contiene il suo albero di lavoro e contiene le sue modifiche padre.

Le modifiche padre contengono le modifiche padre, tutte indietro.

La parte del repository git che contiene la cronologia è quella catena di modifiche. Quella catena di modifiche lo modifica a un livello sopra l'albero "directory" - da un albero "directory", non è possibile accedere in modo univoco a una serie di modifiche e alla catena di modifiche.

Per scoprire cosa succede a un file, si inizia con quel file in un changeset. Quel changeset ha una storia. Spesso in quella cronologia, esiste lo stesso file denominato, a volte con lo stesso contenuto. Se il contenuto è lo stesso, non è stato apportato alcun cambiamento al file. Se è diverso, c'è un cambiamento e il lavoro deve essere fatto per capire esattamente cosa.

A volte il file è sparito; ma l'albero "directory" potrebbe avere un altro file con lo stesso contenuto (stesso codice hash), quindi possiamo seguirlo in quel modo (nota; ecco perché vuoi che un commit commuova un file separato da un commit-to -modificare). O lo stesso nome file e dopo aver verificato il file è abbastanza simile.

Quindi git può patchwork insieme una "cronologia dei file".

Ma questa cronologia dei file deriva dall'analisi efficiente dell'intero "changeset", non da un collegamento da una versione del file a un'altra.


12

"git non tiene traccia dei file" significa sostanzialmente che i commit di git consistono in un'istantanea dell'albero dei file che collega un percorso dell'albero a un "BLOB" e un grafico di commit che traccia la cronologia dei commit . Tutto il resto viene ricostruito al volo da comandi come "git log" e "git blame". Questa ricostruzione può essere raccontata attraverso varie opzioni quanto dovrebbe essere difficile cercare modifiche basate su file. L'euristica predefinita può determinare quando un BLOB cambia posto nella struttura dei file senza modifiche o quando un file è associato a un BLOB diverso rispetto a prima. I meccanismi di compressione che Git utilizza non si preoccupano molto dei limiti di BLOB / file. Se il contenuto è già da qualche parte, ciò manterrà piccola la crescita del repository senza associare i vari BLOB.

Questo è il repository. Git ha anche un albero di lavoro e in questo albero di lavoro ci sono file tracciati e non tracciati. Solo i file tracciati vengono registrati nell'indice (area di gestione temporanea? Cache?) E solo ciò che viene tracciato viene inserito nel repository.

L'indice è orientato ai file e ci sono alcuni comandi orientati ai file per manipolarlo. Ma ciò che finisce nel repository è solo il commit sotto forma di istantanee dell'albero dei file e dei dati BLOB associati e degli antenati del commit.

Poiché Git non tiene traccia delle cronologie e delle ridenominazioni dei file e la sua efficienza non dipende da esse, a volte devi provare alcune volte con opzioni diverse fino a quando Git non produce la cronologia / le differenze / le critiche a cui sei interessato per storie non banali.

È diverso con sistemi come Subversion che registrano piuttosto che ricostruire storie. Se non è registrato, non puoi sentirne parlare.

In realtà ho creato un programma di installazione differenziale in una sola volta che ha appena confrontato gli alberi di rilascio controllandoli in Git e quindi producendo uno script che duplicava il loro effetto. Poiché a volte venivano spostati interi alberi, ciò produceva installatori differenziali molto più piccoli rispetto alla sovrascrittura / eliminazione di tutto ciò che avrebbe prodotto.


7

Git non tiene traccia di un file direttamente, ma tiene traccia delle istantanee del repository e queste istantanee sono costituite da file.

Ecco un modo per vederlo.

In altri sistemi di controllo della versione (SVN, Rational ClearCase), è possibile fare clic con il pulsante destro del mouse su un file e ottenere la cronologia delle modifiche .

In Git, non esiste un comando diretto che lo faccia. Vedere questa domanda . Rimarrai sorpreso da quante diverse risposte ci sono. Non esiste una risposta semplice perché Git non tiene semplicemente traccia di un file , non nel modo in cui lo fa SVN o ClearCase.


5
Penso di ottenere quello che stai cercando di dire, ma "In Git, non esiste un comando diretto che lo fa" è direttamente contraddetto dalle risposte alla domanda a cui ti sei collegato. Mentre è vero che il controllo delle versioni avviene a livello dell'intero repository, in genere ci sono molti modi per ottenere qualcosa in Git, quindi avere più comandi per mostrare la cronologia di un file non è una prova di molto.
Joe Lee-Moyet,

Ho scremato le prime poche risposte alla domanda che hai collegato e tutte usano git logo qualche programma costruito sopra a quello (o qualche alias che fa la stessa cosa). Ma anche se ci fossero molti modi diversi, come dice Joe, questo è vero anche per mostrare la storia del ramo. ( git log -p <file>è anche integrato e fa esattamente questo)
Voo

Sei sicuro che SVN memorizza internamente le modifiche per file? Non lo uso già da un po 'di tempo, ma ricordo vagamente di avere file denominati come ID di versione, piuttosto che riflettere la struttura dei file di progetto.
Artur Biesiadowski l'

3

Tracciare "contenuti", per inciso, è ciò che ha portato a non tracciare directory vuote.
Ecco perché, se si fornisce l'ultimo file di una cartella, la cartella stessa viene eliminata .

Non è sempre stato così, e solo Git 1.4 (maggio 2006) ha applicato la politica di "tracciamento dei contenuti" con commit 443f833 :

stato git: salta le directory vuote e aggiungi -u per mostrare tutti i file non tracciati

Per impostazione predefinita, usiamo --others --directoryper mostrare le directory non interessanti (per attirare l'attenzione dell'utente) senza il loro contenuto (per liberare l'output).
Mostrare directory vuote non ha senso, quindi passa --no-empty-directoryquando lo facciamo.

Dare -u(o --untracked) disabilita questo disordine per consentire all'utente di ottenere tutti i file non tracciati.

È stato ripetuto anni dopo, nel gennaio 2011, con commit 8fe533 , Git v1.7.4:

Questo è in linea con la filosofia generale dell'interfaccia utente: git tiene traccia dei contenuti, non delle directory vuote.

Nel frattempo, con Git 1.4.3 (settembre 2006), Git inizia a limitare il contenuto non tracciato a cartelle non vuote, con commit 2074cb0 :

non dovrebbe elencare il contenuto di directory completamente non tracciate, ma solo il nome di quella directory (più un finale ' /').

Tracciare il contenuto è ciò che ha permesso a git biasimare, molto presto (Git 1.4.4, ottobre 2006, commit cee7f24 ) di essere più performante:

Ancora più importante, la sua struttura interna è progettata per supportare più facilmente il movimento dei contenuti (noto anche come taglia e incolla) consentendo di prendere più di un percorso dallo stesso commit.

Questo (monitoraggio del contenuto) è anche ciò che aggiunge git add nell'API Git, con Git 1.5.0 (dicembre 2006, commit 366bfcb )

rendere 'git add' un'interfaccia user friendly di prima classe all'indice

Questo porta in primo piano la potenza dell'indice usando un modello mentale adeguato senza parlare affatto dell'indice.
Vedi ad esempio come tutta la discussione tecnica è stata evacuata dalla pagina man di git-add.

Qualsiasi contenuto da impegnare deve essere aggiunto insieme.
Non importa se quel contenuto proviene da nuovi file o file modificati.
Devi solo "aggiungerlo", sia con git-add, sia fornendo git-commit con -a(solo per i file già noti).

Questo è ciò che ha reso git add --interactivepossibile, con lo stesso Git 1.5.0 ( commit 5cde71d )

Dopo aver effettuato la selezione, rispondere con una riga vuota per mettere in scena il contenuto dei file dell'albero di lavoro per i percorsi selezionati nell'indice.

Questo è anche il motivo per cui, per rimuovere in modo ricorsivo tutto il contenuto da una directory, è necessario passare l' -ropzione, non solo il nome della directory come <path>(ancora Git 1.5.0, commit 9f95069 ).

Vedere il contenuto del file anziché il file stesso è ciò che consente lo scenario di unione come quello descritto in commit 1de70db (Git v2.18.0-rc0, aprile 2018)

Considera la seguente unione con un conflitto di rinomina / aggiungi:

  • lato A: modifica foo, aggiungi non correlatobar
  • lato B: rinomina foo->bar(ma non modificare la modalità o i contenuti)

In questo caso, l'unione a tre vie di foo originale, foo di A e B barsi tradurrà in un percorso desiderato barcon lo stesso modo / contenuto che A aveva per foo.
Pertanto, A aveva la modalità e i contenuti giusti per il file e aveva il giusto percorso presente (vale a dire, bar).

Commit 37b65ce , Git v2.21.0-rc0, dicembre 2018, recentemente migliorato la risoluzione di conflitti in conflitto.
E commit bbafc9c firther illustra l'importanza di considerare il contenuto del file , migliorando la gestione dei conflitti di rinomina / rinomina (2to1):

  • Invece di archiviare i file in collide_path~HEADe collide_path~MERGE, i file vengono uniti e registrati in due modi collide_path.
  • Invece di registrare la versione del file rinominato esistente sul lato rinominato nell'indice (ignorando in tal modo eventuali modifiche apportate al file sul lato della cronologia senza rinominare), facciamo una fusione di contenuto a tre vie sul rinominato percorso, quindi memorizzalo nella fase 2 o nella fase 3.
  • Si noti che poiché l'unione del contenuto per ogni ridenominazione può avere conflitti e quindi dobbiamo unire i due file rinominati, possiamo finire con marcatori di conflitto nidificati.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.