Compare commits
5 Commits
0aab5db4a3
...
79643104f2
Author | SHA1 | Date |
---|---|---|
Pacman Ghost | 79643104f2 | 2 years ago |
Pacman Ghost | 98f694537c | 2 years ago |
Pacman Ghost | a68c059aff | 2 years ago |
Pacman Ghost | 86c956d783 | 2 years ago |
Pacman Ghost | f83fc6aa5f | 2 years ago |
@ -0,0 +1,9 @@ |
||||
A small library that examines the internal files of a git repo. |
||||
|
||||
It can handle: |
||||
- objects, stored loose or in packs (including ref and offset deltas) |
||||
- the staging index |
||||
- refs |
||||
- reflogs |
||||
|
||||
Compile the `cli` project (a CLI wrapper around the main library), then run it with `--help` to get help. |
@ -0,0 +1,93 @@ |
||||
namespace git_guts |
||||
|
||||
open System |
||||
open System.IO |
||||
open System.Text.RegularExpressions |
||||
open Spectre.Console |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
type LogEntry = |
||||
{ |
||||
entryType: string option |
||||
prevRef: string option |
||||
nextRef: string option |
||||
userName: string |
||||
userEmail: string |
||||
tstamp: int * string // epoch timestamp + timezone |
||||
msg: string option |
||||
} |
||||
|
||||
member this.dumpLogEntry = |
||||
AnsiConsole.Markup( "[cyan]{0}[/]", |
||||
if this.entryType.IsSome then this.entryType.Value else "(no type)" |
||||
) |
||||
if this.msg.IsSome then |
||||
printf ": %s" this.msg.Value |
||||
printfn "" |
||||
let dt = parseTimestamp ( fst this.tstamp ) |
||||
let tstamp = sprintf "%s %s" (dt.ToString "yyyy-MM-dd HH:mm:ss") (snd this.tstamp) |
||||
printfn "%s (%s) %s" this.userName this.userEmail tstamp |
||||
if this.prevRef.IsSome then |
||||
AnsiConsole.Markup( "{0} ", objNameStr this.prevRef.Value ) |
||||
printf "->" |
||||
if this.nextRef.IsSome then |
||||
AnsiConsole.Markup( " {0}", objNameStr this.nextRef.Value ) |
||||
printfn "" |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<AutoOpen>] |
||||
module Logs = |
||||
|
||||
let internal _findLogFiles repoDir = seq { |
||||
// find log files in the specified repo |
||||
let dname = Path.Join( repoDir, "/.git/logs" ) |
||||
if Directory.Exists( dname ) then |
||||
let prefix = dname + Path.DirectorySeparatorChar.ToString() |
||||
for fname in Directory.GetFiles( dname, "*", SearchOption.AllDirectories ) do |
||||
if not ( fname.StartsWith( prefix ) ) then |
||||
failwithf "Unexpected log filename: %s" fname |
||||
let ref = fname.Substring( prefix.Length ) |
||||
yield ref, fname |
||||
} |
||||
|
||||
let internal _readLogFile fname = seq { |
||||
let regex = Regex( @"^([0-9a-f]{40}) ([0-9a-f]{40}) (.+?) \<(.+?)\> (\d+) ([+-]\d{4})(\s+[^:]+)?" ) |
||||
for line in File.ReadLines( fname ) do |
||||
let line2 = line.Trim() |
||||
let matches = regex.Matches( line2 ) |
||||
if matches.Count <> 1 then |
||||
failwithf "Couldn't parse log line: %s" line2 |
||||
let groups = matches.[0].Groups |
||||
let prevRef, nextRef = groups.[1].Value, groups.[2].Value |
||||
let userName, userEmail = groups.[3].Value, groups.[4].Value |
||||
let tstamp, tzone = groups.[5].Value, groups.[6].Value |
||||
let entryType = if groups.[7].Success then Some( groups.[7].Value.Trim() ) else None |
||||
let msg = |
||||
if groups.[0].Length < line2.Length then |
||||
Some ( line2.Substring( groups.[0].Length + 2 ) ) |
||||
else |
||||
None |
||||
let checkRef ref = |
||||
if ref = "0000000000000000000000000000000000000000" then None else Some ref |
||||
yield { |
||||
entryType = entryType |
||||
prevRef = checkRef prevRef; nextRef = checkRef nextRef |
||||
userName = userName; userEmail = userEmail |
||||
tstamp = ( int(tstamp), tzone ) |
||||
msg = msg |
||||
} |
||||
} |
||||
|
||||
let dumpLogs repoDir = |
||||
|
||||
// dump the log files (sorted, for stable output) |
||||
_findLogFiles repoDir |> Seq.sortBy ( fun f -> fst f ) |> Seq.iteri ( fun logNo (ref, fname) -> |
||||
if logNo > 0 then |
||||
printfn "" |
||||
AnsiConsole.MarkupLine( makeHeader ref "" ) |
||||
for entry in _readLogFile fname do |
||||
printfn "" |
||||
entry.dumpLogEntry |
||||
) |
@ -0,0 +1,66 @@ |
||||
namespace git_guts |
||||
|
||||
open System.IO |
||||
open System.Text |
||||
|
||||
open Spectre.Console |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<AutoOpen>] |
||||
module Refs = |
||||
|
||||
let private _findLooseRefs repoDir refType = seq { |
||||
// find loose refs in the git repo |
||||
let refsDir = Path.Join( repoDir, ".git/refs", refType ) |
||||
if Directory.Exists refsDir then |
||||
for fname in Directory.GetFiles( refsDir ) do |
||||
let objName = File.ReadAllText( fname, Encoding.ASCII ).Trim() |
||||
yield ( Path.GetFileName fname, objName ) |
||||
} |
||||
|
||||
let private _findPackedRefs repoDir = seq { |
||||
// find packed refs in the git repo |
||||
let fname = Path.Join( repoDir, ".git/packed-refs" ) |
||||
if File.Exists fname then |
||||
let mutable currRef = None |
||||
for line in File.ReadLines( fname ) do |
||||
let line2 = line.Trim() |
||||
if line2.Length > 0 && line2.[0] <> '#' then |
||||
if line2.[0] = '^' then |
||||
// the previous line is an annotated tag, this line is the target commit |
||||
yield ( fst currRef.Value, snd currRef.Value, Some (line2.Substring 1) ) |
||||
currRef <- None |
||||
else |
||||
// the previous line was a normal tag - we can now return it to the caller |
||||
if currRef.IsSome then |
||||
yield ( fst currRef.Value, snd currRef.Value, None ) |
||||
// save the current line (to be yield'ed later) |
||||
// NOTE: We can't yield the tag now because it might be an annotated tag, |
||||
// in which case we need to wait for the next line to get the target commit. |
||||
currRef <- Some ( line2.Substring(41), line2.Substring(0,40) ) |
||||
if currRef.IsSome then |
||||
yield ( fst currRef.Value, snd currRef.Value, None ) |
||||
} |
||||
|
||||
let dumpRefs repoDir = |
||||
|
||||
// dump the loose refs |
||||
for refType in [| "heads"; "tags" |] do |
||||
AnsiConsole.MarkupLine( makeHeader refType "" ) |
||||
let looseRefs = _findLooseRefs repoDir refType |> Seq.sortBy ( fun r -> fst r ) |> Seq.toList |
||||
if looseRefs.Length > 0 then |
||||
printfn "" |
||||
for ref, objName in looseRefs do |
||||
AnsiConsole.MarkupLine( "{0} -> {1}", refStr ref, objNameStr objName ) |
||||
printfn "" |
||||
|
||||
// dump the packed refs |
||||
AnsiConsole.MarkupLine( makeHeader "packed refs" "" ) |
||||
printfn "" |
||||
let packedRefs = _findPackedRefs repoDir |> Seq.sortBy ( fun (r, _, _) -> r ) |
||||
for ref, objName, target in packedRefs do |
||||
AnsiConsole.Markup( "{0} -> {1}", refStr ref, objNameStr objName ) |
||||
if target.IsSome then |
||||
AnsiConsole.Markup( " -> {0}", objNameStr target.Value ) |
||||
printfn "" |
@ -0,0 +1,309 @@ |
||||
namespace git_guts |
||||
|
||||
open System |
||||
open System.IO |
||||
open System.Text |
||||
|
||||
open Spectre.Console |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
// This records a single entry in a repo's staging index. |
||||
type StagingIndexEntry = |
||||
{ |
||||
ctime: int * int // nb: seconds + nanoseconds |
||||
mtime: int * int // nb: seconds + nanoseconds |
||||
dev: int |
||||
ino: int |
||||
objType: int |
||||
perms: uint16 |
||||
uid: int |
||||
gid: int |
||||
fileSize: int |
||||
objName: string |
||||
flags: uint16 |
||||
extendedFlags: uint16 option |
||||
path: byte[] // nb: the encoding is unknown |
||||
} |
||||
|
||||
static member private _OBJECT_TYPES = Map[ ( 0x08, "regular file" ); ( 0x0a, "symlink" ); ( 0x0e, "gitlink" ) ] |
||||
static member private _FLAG_NAMES = Map[ (0x8000u, "assume-valid"); (0x4000u, "extended") ] |
||||
static member private _EXTENDED_FLAG_NAMES = Map[ (0x4000u, "skip-worktree"); (0x2000u, "intent-to-add") ] |
||||
|
||||
member private this._flagsStr = |
||||
// return the StagingIndexEntry's flags as a string |
||||
let mutable vals = [] |
||||
let flags = uint( this.flags ) |
||||
let bflags = bitflagString flags 2 StagingIndexEntry._FLAG_NAMES |
||||
if bflags.Length >= 10 then |
||||
vals <- vals @ [ bflags.[ 0 .. bflags.Length-10 ] ] |
||||
vals <- vals @ [ sprintf "stage=%d" ((flags &&& 0x3000u) >>> 12) ] |
||||
// NOTE: The "name length" field is for the entry path name, not the object name. |
||||
let namelen = flags &&& 0x0fffu |
||||
vals <- vals @ [ |
||||
if namelen < 0xfffu then sprintf "namelen=%d" namelen else "namelen=0xFFF" |
||||
] |
||||
let valsStr = String.Join( ", ", vals ) |
||||
let bflags2 = if bflags.Length >= 8 then bflags.Substring( bflags.Length-7, 6 ) else bflags |
||||
sprintf "%s (%s)" valsStr bflags2 |
||||
|
||||
member private this._xflagsStr = |
||||
// return the StagingIndexEntry's extended flags as a string |
||||
let xflags = uint( this.extendedFlags.Value ) |
||||
bitflagString xflags 2 StagingIndexEntry._EXTENDED_FLAG_NAMES |
||||
|
||||
member this.dumpObj fullDump = |
||||
// NOTE: The encoding for the path is actually unknown :-/ Encoding.UTF8 uses replacement fallback. |
||||
AnsiConsole.MarkupLine( "- path: {0}", pathStr (Encoding.UTF8.GetString( this.path )) ) |
||||
AnsiConsole.MarkupLine( "- name: {0}", objNameStr this.objName ) |
||||
printfn "- flags: %s" this._flagsStr |
||||
if this.extendedFlags.IsSome then |
||||
printfn " %s" this._xflagsStr |
||||
printfn "- type: %s (%d)" StagingIndexEntry._OBJECT_TYPES.[this.objType] this.objType |
||||
printfn "- size: %d" this.fileSize |
||||
printfn "- perms: %s" (permsString this.perms) |
||||
if fullDump then |
||||
printfn "- uid: %d" this.uid |
||||
printfn "- gid: %d" this.gid |
||||
let makeTimeStr timeVal = |
||||
let epoch = DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ) |
||||
let dt = epoch.AddSeconds( float( fst timeVal ) ) |
||||
sprintf "%s (%d.%09d)" (dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")) (fst timeVal) (snd timeVal) |
||||
printfn "- ctime: %s" (makeTimeStr this.ctime) |
||||
printfn "- mtime: %s" (makeTimeStr this.mtime) |
||||
printfn "- dev: %d" this.dev |
||||
printfn "- ino: %d" this.ino |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<AbstractClass>] |
||||
type StagingIndexExtension () = |
||||
// Base class for extensions stored in the staging index. |
||||
|
||||
abstract member extnSig: string |
||||
abstract member dumpExtn: unit -> unit |
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
// Used to hold a TREE extension. |
||||
type TreeExtensionEntry = |
||||
{ |
||||
path: byte[] // nb: the encoding is unknown |
||||
nEntries: int |
||||
nSubTrees: int |
||||
objName: string option |
||||
} |
||||
|
||||
member this.dumpEntry = |
||||
// NOTE: The encoding for the path is actually unknown :-/ Encoding.UTF8 uses replacement fallback. |
||||
AnsiConsole.MarkupLine( "- path = {0}", pathStr ( Encoding.UTF8.GetString this.path ) ) |
||||
printfn " - entries: %d" this.nEntries |
||||
printfn " - subtrees: %d" this.nSubTrees |
||||
if this.objName.IsSome then |
||||
AnsiConsole.MarkupLine( " - name: {0}", objNameStr this.objName.Value ) |
||||
|
||||
type TreeExtension( extnData: byte[] ) = |
||||
inherit StagingIndexExtension () |
||||
|
||||
override this.extnSig = "TREE" |
||||
|
||||
member val entries = |
||||
// parse the TREE extension data |
||||
use extnDataBuf = new MemoryStream( extnData ) |
||||
Seq.initInfinite ( fun _ -> |
||||
if extnDataBuf.Position < extnDataBuf.Length then |
||||
let path = readUntil extnDataBuf 0uy |
||||
let nEntries = int( Encoding.ASCII.GetString( readUntil extnDataBuf 0x20uy ) ) |
||||
let nSubTrees = int( Encoding.ASCII.GetString( readUntil extnDataBuf 0x0auy ) ) |
||||
let objName = if nEntries <> -1 then Some( readObjName extnDataBuf ) else None |
||||
Some { path=path; nEntries=nEntries; nSubTrees=nSubTrees; objName=objName } |
||||
else |
||||
None |
||||
) |> Seq.takeWhile ( fun e -> e.IsSome ) |> Seq.map ( fun e -> e.Value ) |> Seq.toArray |
||||
|
||||
override this.dumpExtn () = |
||||
// dump the TREE extension |
||||
for entry in this.entries do |
||||
entry.dumpEntry |
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
// Used to hold a REUC extension. |
||||
type ReucExtensionEntry = |
||||
{ |
||||
path: byte[] // nb: the encoding is unknown |
||||
stages: ( string * string option )[] // perms + object name |
||||
} |
||||
|
||||
member this.dumpEntry = |
||||
// NOTE: The encoding for the path is actually unknown :-/ Encoding.UTF8 uses replacement fallback. |
||||
AnsiConsole.MarkupLine( "- path = {0}", pathStr ( Encoding.UTF8.GetString this.path ) ) |
||||
for i = 0 to this.stages.Length-1 do |
||||
let perms, objName = this.stages.[i] |
||||
let objName2 = if objName.IsSome then sprintf " %s" objName.Value else "" |
||||
AnsiConsole.MarkupLine( " - stage {0}: {1}{2}", i+1, perms, objNameStr objName2 ) |
||||
|
||||
type ReucExtension( extnData: byte[] ) = |
||||
inherit StagingIndexExtension () |
||||
|
||||
override this.extnSig = "REUC" |
||||
|
||||
member val entries = |
||||
// parse the REUC extension data |
||||
use extnDataBuf = new MemoryStream( extnData ) |
||||
Seq.initInfinite ( fun _ -> |
||||
if extnDataBuf.Position < extnDataBuf.Length then |
||||
let path = readUntil extnDataBuf 0uy |
||||
let getMode _ = |
||||
readString extnDataBuf "ascii" // nb: these are ASCII octal numbers |
||||
let modes = Seq.initInfinite getMode |> Seq.take 3 |> Seq.toArray |
||||
let getObjName mode = |
||||
if mode = "0" then None else Some( readObjName extnDataBuf ) |
||||
let objNames = modes |> Seq.map getObjName |> Seq.toArray |
||||
let stages = Array.zip modes objNames |
||||
Some { path=path; stages=stages } |
||||
else |
||||
None |
||||
) |> Seq.takeWhile ( fun e -> e.IsSome ) |> Seq.map ( fun e -> e.Value ) |> Seq.toArray |
||||
|
||||
override this.dumpExtn () = |
||||
// dump the REUC extension |
||||
for entry in this.entries do |
||||
entry.dumpEntry |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<AutoOpen>] |
||||
module StagingIndex = |
||||
|
||||
let private _readStagingIndexHeader (inp: Stream) = |
||||
|
||||
// read the header |
||||
let buf = readBytes inp 4 |
||||
if (Encoding.ASCII.GetString buf) <> "DIRC" then |
||||
failwithf "Incorrect magic number: %A" buf |
||||
let version = readNboInt4 inp |
||||
if version <> 2 && version <> 3 && version <> 4 then |
||||
failwithf "Unexpected version: %d" version |
||||
|
||||
version |
||||
|
||||
let private _readStagingIndexEntry (inp: Stream) version = |
||||
|
||||
// NOTE: Entries usually represent a file, but can sometimes refer to a directory (if sparse checkout |
||||
// is enabled in cone mode, and the sparse index extension is enabled), in which case: |
||||
// - mode = 040000 |
||||
// - has SKIP_WORKTREE in the extended flags |
||||
// - the path ends with a directory separator |
||||
// IMPORTANT! We assume we're not in split index mode (the entry format is completely different). |
||||
|
||||
let readPath () = |
||||
if version = 4 then |
||||
// NOTE: The path format is completely different in v4, so for simplicity, we don't support it. |
||||
failwith "Version 4 is not supported." |
||||
readUntil inp 0uy |
||||
|
||||
// read the next staging index entry |
||||
let fposStart = inp.Position |
||||
let ctime = ( readNboInt4 inp, readNboInt4 inp ) |
||||
let mtime = ( readNboInt4 inp, readNboInt4 inp ) |
||||
let dev = readNboInt4 inp |
||||
let ino = readNboInt4 inp |
||||
// NOTE: The doco says that the mode field is a 32-bit value, but only accounts for 16 of them :-/ |
||||
let mode = readBytes inp 4 |
||||
let objType = int( mode.[2] &&& 0xf0uy ) >>> 4 |
||||
let perms = uint16( mode.[2] &&& 0x01uy ) <<< 8 ||| uint16(mode.[3]) |
||||
let uid = readNboInt4 inp |
||||
let gid = readNboInt4 inp |
||||
let fileSize = readNboInt4 inp |
||||
let objName = readObjName inp |
||||
let flags = uint16( readNboInt2 inp ) |
||||
let extendedFlags = |
||||
if version >= 3 && flags &&& 0x400us <> 0us then |
||||
Some( uint16( readNboInt2 inp ) ) |
||||
else |
||||
None |
||||
let path = readPath () |
||||
|
||||
// skip over the pad bytes (used to 8-align entries) |
||||
if version <> 4 then |
||||
while (inp.Position - fposStart) % 8L <> 0L do |
||||
inp.ReadByte() |> ignore |
||||
|
||||
// create the StagingIndexEntry record |
||||
let entry = { |
||||
ctime=ctime; mtime=mtime |
||||
dev=dev; ino=ino |
||||
objType=objType |
||||
perms=perms |
||||
uid=uid; gid=gid |
||||
fileSize=fileSize |
||||
objName=objName |
||||
flags=flags; extendedFlags=extendedFlags |
||||
path=path |
||||
} |
||||
|
||||
( entry, fposStart ) |
||||
|
||||
let private _makeStagingIndexExtension extnSig extnData = |
||||
// create a StagingIndexExtension-derived object |
||||
match extnSig with |
||||
| [| 84uy; 82uy; 69uy; 69uy |] -> // "TREE" |
||||
(TreeExtension extnData) :> StagingIndexExtension |
||||
| [|82uy; 69uy; 85uy; 67uy|] -> // "REUC" |
||||
(ReucExtension extnData) :> StagingIndexExtension |
||||
| _ -> failwithf "Unknown extension sig: %A" extnSig |
||||
|
||||
let private _readExtension inp = |
||||
// read the extension data |
||||
let extnSig = readBytes inp 4 |
||||
let nBytes = readNboInt4 inp |
||||
let extnData = readBytes inp nBytes |
||||
let extn = _makeStagingIndexExtension extnSig extnData |
||||
( extn, extnData ) |
||||
|
||||
let dumpStagingIndex repoDir fullDump = |
||||
|
||||
// initialize |
||||
let fname = Path.Join( repoDir, ".git/index" ) |
||||
if not ( File.Exists( fname ) ) then |
||||
failwith "Can't find the staging index file." |
||||
use inp = new FileStream( fname, FileMode.Open, FileAccess.Read, FileShare.Read ) |
||||
|
||||
// dump the header |
||||
let version = _readStagingIndexHeader inp |
||||
let nEntries = readNboInt4 inp |
||||
AnsiConsole.MarkupLine( makeHeader "HEADER" "" ) |
||||
printfn "" |
||||
printfn "version: %d" version |
||||
printfn "entries: %d" nEntries |
||||
|
||||
// dump the entries |
||||
printfn "" |
||||
AnsiConsole.MarkupLine( makeHeader "ENTRIES" "" ) |
||||
for entryNo = 0 to nEntries-1 do |
||||
printfn "" |
||||
let entry, fpos = _readStagingIndexEntry inp version |
||||
AnsiConsole.MarkupLine( sprintf "[cyan]Entry %d[/]: fpos=0x%x" entryNo fpos ) |
||||
entry.dumpObj fullDump |
||||
|
||||
// dump the extensions |
||||
let fposEnd = inp.Length - 20L // we ignore the checksum at the end of the file |
||||
Seq.initInfinite ( fun _ -> |
||||
if inp.Position < fposEnd then |
||||
let fposStart = inp.Position |
||||
let extn, extnData = _readExtension inp |
||||
Some ( extn, extnData, fposStart ) |
||||
else |
||||
None |
||||
) |> Seq.takeWhile ( fun e -> e.IsSome ) |> Seq.map ( fun e -> e.Value ) |> Seq.iteri ( fun extnNo row -> |
||||
let extn, extnData, fpos = row |
||||
if extnNo = 0 then |
||||
printfn "" |
||||
AnsiConsole.MarkupLine( makeHeader "EXTENSIONS" "" ) |
||||
printfn "" |
||||
AnsiConsole.MarkupLine( "[cyan]{0}[/]: fpos=0x{1:x}, #bytes={2}", |
||||
extn.extnSig, fpos, extnData.Length |
||||
) |
||||
extn.dumpExtn () |
||||
) |
@ -0,0 +1,183 @@ |
||||
namespace git_guts |
||||
|
||||
open System |
||||
open System.IO |
||||
open System.Text |
||||
open System.Text.RegularExpressions |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<AutoOpen>] |
||||
module VerifyObjects = |
||||
|
||||
let private _adjustGitTreeOutput (objDump: string) = |
||||
// adjust git's output for TREE objects so that it matches what we output |
||||
let regex = Regex( @"^(\d+) [a-z]+ ([0-9a-f]{40})\s+(.+)$" ) |
||||
let buf = new StringBuilder() |
||||
let reader = new StreamReader( new MemoryStream( Encoding.UTF8.GetBytes( objDump ) ) ) |
||||
while not reader.EndOfStream do |
||||
let line = reader.ReadLine() |
||||
let matches = regex.Matches( line ) |
||||
let groups = matches.[0].Groups |
||||
let perms, objName = groups.[1].Value, groups.[2].Value |
||||
let mutable path = groups.[3].Value |
||||
// FUDGE! git outputs UTF-8 bytes as octal-encoded strings, not the bytes themselves?! |
||||
if path.[0] = '"' && path.[path.Length-1] = '"' then |
||||
let rec updatePath (partialPath: string) = seq { |
||||
let pos = partialPath.IndexOf( '\\' ) |
||||
if pos < 0 then |
||||
yield! Encoding.UTF8.GetBytes( partialPath ) |
||||
else |
||||
yield! Encoding.UTF8.GetBytes( partialPath.Substring( 0, pos ) ) |
||||
let n = Convert.ToByte( partialPath.Substring( pos+1, 3 ), 8 ) |
||||
yield byte( n ) |
||||
yield! updatePath ( partialPath.Substring( pos+4 ) ) |
||||
} |
||||
path <- Encoding.UTF8.GetString( |
||||
updatePath ( path.Substring( 1, path.Length-2 ) ) |> Seq.toArray |
||||
) |
||||
buf.AppendFormat( "{0} {1} {2}\n", perms, objName, path ) |> ignore |
||||
buf.ToString().TrimEnd() |
||||
|
||||
let verifyObjects repoDir progress = |
||||
|
||||
// NOTE: This will iterate over every object in a repo, and compare what we retrieve with what |
||||
// "git cat-file" returns. In particular, this will include *every* revision of *every* file, |
||||
// so for large repo's, it will take some time... |
||||
|
||||
// initialize |
||||
disableSpectreCapabilities () |
||||
let mutable currPackFname = "" |
||||
let mutable packObjCounts = Map[ ( "", 0 ) ] |
||||
|
||||
let onEndPackFile () = |
||||
let nObjs = packObjCounts.[ currPackFname ] |
||||
printfn "- Checked %s." ( plural nObjs "object" "objects" ) |
||||
|
||||
// check each object in the repo |
||||
printfn "Checking loose objects..." // nb: because getObjNames returns loose objects first |
||||
for objName, fname in getObjNames repoDir do |
||||
|
||||
// check if we have a loose object or an object in a pack |
||||
let packFname = |
||||
if Path.GetExtension( fname ) = ".pack" then Path.GetFileName( fname ) else "" |
||||
|
||||
// check if we've started a new pack |
||||
if packFname <> currPackFname then |
||||
// yup - log the end of the current one |
||||
onEndPackFile () |
||||
// prepare to start processing the new pack file |
||||
currPackFname <- packFname |
||||
packObjCounts <- packObjCounts.Add ( currPackFname, 0 ) |
||||
let fsize = FileInfo( fname ).Length |
||||
printfn "" |
||||
printfn "Checking pack file (%s): %s" (friendlyByteCount fsize) currPackFname |
||||
|
||||
// find the next object |
||||
if progress then |
||||
eprintfn "- Checking object: %s" objName |
||||
let objRec = _findRepoObjRec repoDir objName |
||||
if objRec.IsNone then |
||||
failwithf "Can't find object: %s" objName |
||||
let mutable objData = objRec.Value.objData |
||||
let obj = makeGitObject objRec.Value |
||||
packObjCounts <- packObjCounts.Add ( currPackFname, packObjCounts.[currPackFname]+1 ) |
||||
if progress then |
||||
eprintfn " - Got %s: #bytes=%d" obj.objType objData.Length |
||||
|
||||
// check the object type |
||||
let expectedObjType = ( runGitText repoDir "cat-file" [ "-t"; objName ] ).TrimEnd() |
||||
if obj.objType <> expectedObjType then |
||||
failwithf "Object type mismatch for %s: got \"%s\", expected \"%s\"." objName obj.objType expectedObjType |
||||
|
||||
// check the object data |
||||
let mutable expectedObjData = ( runGit repoDir "cat-file" [ "-p"; objName ] ) |
||||
if obj.objType = "tree" then |
||||
objData <- Encoding.UTF8.GetBytes( |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
obj.dumpObj() |
||||
cap.getOutput.TrimEnd() |
||||
) |
||||
) |
||||
expectedObjData <- Encoding.UTF8.GetBytes( |
||||
_adjustGitTreeOutput ( Encoding.UTF8.GetString expectedObjData ) |
||||
) |
||||
if objData <> expectedObjData then |
||||
let dname = Path.GetTempPath() |
||||
File.WriteAllBytes( Path.Join( dname, "git-content.expected" ), expectedObjData ) |
||||
File.WriteAllBytes( Path.Join( dname, "git-content.actual" ), objData ) |
||||
failwithf "Object data mismatch for %s." objName |
||||
|
||||
onEndPackFile () |
||||
|
||||
// NOTE: These functions generate object names that are invalid (they contain a non-hex character), |
||||
// but they will always compare greater/less than a valid name, based on the first byte, which will |
||||
// help test how we use the fanout table, and the binary search through the table of object names. |
||||
let makeObjName1 byte0 = |
||||
sprintf "%02x%s!" byte0 (String('0',37)) |
||||
let makeObjName2 byte0 = |
||||
sprintf "%02x%sz" byte0 (String('f',37)) |
||||
// NOTE: Also test with an object name that appears in the middle of the range, for a given first byte. |
||||
let makeObjName3 byte0 = |
||||
sprintf "%02x%s" byte0 "80808080808080808080808080808080808080" |
||||
|
||||
// verify looking up unknown objects |
||||
printfn "" |
||||
printfn "Checking unknown objects..." |
||||
let mutable nObjs = 0 |
||||
[| makeObjName1; makeObjName2; makeObjName3 |] |> Seq.iter ( fun makeObjName -> |
||||
for byte0 = 0 to 255 do |
||||
let objName = makeObjName byte0 |
||||
let obj = findRepoObject repoDir objName |
||||
if obj.IsSome then |
||||
failwithf "Unexpectedly found object: %s" objName |
||||
nObjs <- nObjs + 1 |
||||
) |
||||
printf "- Checked %s." ( plural nObjs "unknown object" "unknown objects." ) |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<AutoOpen>] |
||||
module VerifyLogs = |
||||
|
||||
let verifyLogs repoDir = |
||||
|
||||
// verify reading each log file |
||||
for ref, fname in _findLogFiles repoDir do |
||||
printfn "Processing log file: %s" fname |
||||
|
||||
// run git to get the log entries for the current ref |
||||
let ref2 = |
||||
let ref2 = ref.Replace( Path.DirectorySeparatorChar, '/' ) |
||||
if ref2.Length >= 12 && ref2.Substring( 0, 11 ) = "refs/heads/" then |
||||
ref2.Substring( 11 ) |
||||
else |
||||
ref2 |
||||
let expected = ( runGitText repoDir "reflog" [| "show"; ref2 |] ).TrimEnd() |
||||
|
||||
// NOTE: git shows just enough of the object names for them to be unique, so we need |
||||
// to figure out how much that is, so that we can generate the same output :-/ |
||||
// We assume the first line is a log entry, that starts with an abbreviated object name. |
||||
let objNamePrefixLen = expected.IndexOf( ' ' ) |
||||
|
||||
// extract the log entries for the current ref |
||||
let buf = new StringBuilder() |
||||
let mutable nLogEntries = 0 |
||||
_readLogFile fname |> Seq.rev |> Seq.iteri ( fun logEntryNo logEntry -> |
||||
if logEntry.nextRef.IsSome then |
||||
let objNamePrefix = logEntry.nextRef.Value.Substring( 0, objNamePrefixLen ) |
||||
buf.AppendFormat( "{0} {1}@{{{2}}}: {3}", objNamePrefix, ref2, logEntryNo, logEntry.entryType.Value ) |> ignore |
||||
if logEntry.msg.IsSome then |
||||
buf.AppendFormat( ": {0}", logEntry.msg.Value ) |> ignore |
||||
buf.AppendLine( "" ) |> ignore |
||||
nLogEntries <- nLogEntries + 1 |
||||
) |
||||
let output = buf.ToString().TrimEnd() |
||||
|
||||
// compare what we extracted with the git output |
||||
if output <> expected then |
||||
let dname = Path.GetTempPath() |
||||
File.WriteAllText( Path.Join( dname, "git-log.expected" ), expected ) |
||||
File.WriteAllText( Path.Join( dname, "git-log.actual" ), output ) |
||||
failwithf "Mismatched output for ref: %s" ref2 |
||||
printfn "- Checked %s." ( plural nLogEntries "log entry" "log entries" ) |
@ -0,0 +1,41 @@ |
||||
namespace tests |
||||
|
||||
open System |
||||
open System.IO |
||||
open Microsoft.VisualStudio.TestTools.UnitTesting |
||||
|
||||
open git_guts |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<TestClass>] |
||||
type TestStagingIndex () = |
||||
|
||||
[<TestInitialize>] |
||||
member this.init () = |
||||
// prepare to run a test |
||||
disableSpectreCapabilities () |
||||
|
||||
[<TestMethod>] |
||||
member this.TestDumpStagingIndex () = |
||||
|
||||
let doTest zipFname = |
||||
|
||||
// set up the test repo |
||||
use gitTestRepo = new GitTestRepo( zipFname ) |
||||
runGitGc gitTestRepo.repoDir |
||||
|
||||
// dump the staging index |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
dumpStagingIndex gitTestRepo.repoDir false |
||||
let expectedFname = |
||||
let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".staging-index.txt" |
||||
Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) |
||||
cap.checkOutput expectedFname |
||||
) |
||||
|
||||
// run the tests |
||||
Assert.ThrowsException<Exception>( fun () -> |
||||
doTest "empty.zip" |
||||
) |> ignore |
||||
doTest "simple.zip" |
@ -0,0 +1,39 @@ |
||||
namespace tests |
||||
|
||||
open System |
||||
open System.IO |
||||
open Microsoft.VisualStudio.TestTools.UnitTesting |
||||
|
||||
open git_guts |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<TestClass>] |
||||
type TestLogs () = |
||||
|
||||
[<TestInitialize>] |
||||
member this.init () = |
||||
// prepare to run a test |
||||
disableSpectreCapabilities () |
||||
|
||||
[<TestMethod>] |
||||
member this.TestDumpLogs () = |
||||
|
||||
let doTest zipFname = |
||||
|
||||
// set up the test repo |
||||
use gitTestRepo = new GitTestRepo( zipFname ) |
||||
|
||||
// dump the logs |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
dumpLogs gitTestRepo.repoDir |
||||
let expectedFname = |
||||
let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".logs.txt" |
||||
Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) |
||||
checkCapturedOutput cap expectedFname |
||||
) |
||||
|
||||
// run the tests |
||||
doTest "empty.zip" |
||||
doTest "simple.zip" |
||||
doTest "full2.zip" |
@ -0,0 +1,48 @@ |
||||
namespace tests |
||||
|
||||
open System |
||||
open System.IO |
||||
open Microsoft.VisualStudio.TestTools.UnitTesting |
||||
|
||||
open git_guts |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<TestClass>] |
||||
type TestRefs () = |
||||
|
||||
[<TestInitialize>] |
||||
member this.init () = |
||||
// prepare to run a test |
||||
disableSpectreCapabilities () |
||||
|
||||
[<TestMethod>] |
||||
member this.TestDumpRefs () = |
||||
|
||||
let doTest zipFname = |
||||
|
||||
// set up the test repo |
||||
use gitTestRepo = new GitTestRepo( zipFname ) |
||||
|
||||
// dump the refs |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
dumpRefs gitTestRepo.repoDir |
||||
let expectedFname = |
||||
let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".refs.txt" |
||||
Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) |
||||
checkCapturedOutput cap expectedFname |
||||
) |
||||
|
||||
// move the loose objects to a pack, and check again |
||||
runGitGc gitTestRepo.repoDir |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
dumpRefs gitTestRepo.repoDir |
||||
let expectedFname = |
||||
let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".refs-packed.txt" |
||||
Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) |
||||
checkCapturedOutput cap expectedFname |
||||
) |
||||
|
||||
// run the tests |
||||
doTest "empty.zip" |
||||
doTest "full2.zip" |
@ -0,0 +1,49 @@ |
||||
namespace tests |
||||
|
||||
open System |
||||
open System.IO |
||||
open Microsoft.VisualStudio.TestTools.UnitTesting |
||||
|
||||
open git_guts |
||||
|
||||
// -------------------------------------------------------------------- |
||||
|
||||
[<TestClass>] |
||||
type TestVerify () = |
||||
|
||||
[<TestInitialize>] |
||||
member this.init () = |
||||
// prepare to run a test |
||||
disableSpectreCapabilities () |
||||
|
||||
[<TestMethod>] |
||||
member this.TestVerify () = |
||||
|
||||
let doChecks repoDir = |
||||
|
||||
// verify retrieving objects from the repo |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
verifyObjects repoDir false |
||||
) |
||||
|
||||
// verify retrieving logs from the repo |
||||
using ( new CaptureStdout() ) ( fun cap -> |
||||
verifyLogs repoDir |
||||
) |
||||
|
||||
let doTest zipFname = |
||||
|
||||
// set up the test repo |
||||
use gitTestRepo = new GitTestRepo( zipFname ) |
||||
|
||||
// do the checks |
||||
doChecks gitTestRepo.repoDir |
||||
|
||||
// run garbage collection, and verify again |
||||
runGitGc gitTestRepo.repoDir |
||||
doChecks gitTestRepo.repoDir |
||||
|
||||
// run the tests |
||||
let dname = Path.Combine( __SOURCE_DIRECTORY__, "fixtures" ) |
||||
for fname in Directory.GetFiles( dname, "*.zip", SearchOption.AllDirectories ) do |
||||
doTest ( Path.GetFileName fname ) |
@ -0,0 +1,6 @@ |
||||
--- heads ---------------------------------------------------------------------- |
||||
|
||||
--- tags ----------------------------------------------------------------------- |
||||
|
||||
--- packed refs ---------------------------------------------------------------- |
||||
|
@ -0,0 +1,6 @@ |
||||
--- heads ---------------------------------------------------------------------- |
||||
|
||||
--- tags ----------------------------------------------------------------------- |
||||
|
||||
--- packed refs ---------------------------------------------------------------- |
||||
|
Binary file not shown.
@ -0,0 +1,57 @@ |
||||
--- HEAD ----------------------------------------------------------------------- |
||||
|
||||
commit (initial): Added a file with spaces in its path. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:36:07 +0000 |
||||
-> de483a0a2160dc698967033f2c351208e6da9066 |
||||
|
||||
commit: Added a binary file. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:36:31 +0000 |
||||
de483a0a2160dc698967033f2c351208e6da9066 -> 89bac892dbe3bc028ec2c361d836791f446ffc11 |
||||
|
||||
commit: Added an empty file. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:36:46 +0000 |
||||
89bac892dbe3bc028ec2c361d836791f446ffc11 -> fb407a730e3730eb07698f7ced84b596fe7fb7bd |
||||
|
||||
commit: Added a file with Unicode (日本) in its name and content. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:37:10 +0000 |
||||
fb407a730e3730eb07698f7ced84b596fe7fb7bd -> 30c84cc603759f82470a119c5b5348001ab13500 |
||||
|
||||
checkout: moving from master to a-branch |
||||
Taka (tutorial@git-guts) 2021-12-29 03:52:26 +0000 |
||||
30c84cc603759f82470a119c5b5348001ab13500 -> de483a0a2160dc698967033f2c351208e6da9066 |
||||
|
||||
commit: Changed the greeting. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:53:14 +0000 |
||||
de483a0a2160dc698967033f2c351208e6da9066 -> 06d4e9c2beb2c23a955bdb213d02ac63e15e3318 |
||||
|
||||
checkout: moving from a-branch to master |
||||
Taka (tutorial@git-guts) 2021-12-29 03:54:11 +0000 |
||||
06d4e9c2beb2c23a955bdb213d02ac63e15e3318 -> 30c84cc603759f82470a119c5b5348001ab13500 |
||||
|
||||
--- refs/heads/a-branch -------------------------------------------------------- |
||||
|
||||
branch: Created from de483a0a2160dc698967033f2c351208e6da9066 |
||||
Taka (tutorial@git-guts) 2021-12-29 03:52:26 +0000 |
||||
-> de483a0a2160dc698967033f2c351208e6da9066 |
||||
|
||||
commit: Changed the greeting. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:53:14 +0000 |
||||
de483a0a2160dc698967033f2c351208e6da9066 -> 06d4e9c2beb2c23a955bdb213d02ac63e15e3318 |
||||
|
||||
--- refs/heads/master ---------------------------------------------------------- |
||||
|
||||
commit (initial): Added a file with spaces in its path. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:36:07 +0000 |
||||
-> de483a0a2160dc698967033f2c351208e6da9066 |
||||
|
||||
commit: Added a binary file. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:36:31 +0000 |
||||
de483a0a2160dc698967033f2c351208e6da9066 -> 89bac892dbe3bc028ec2c361d836791f446ffc11 |
||||
|
||||
commit: Added an empty file. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:36:46 +0000 |
||||
89bac892dbe3bc028ec2c361d836791f446ffc11 -> fb407a730e3730eb07698f7ced84b596fe7fb7bd |
||||
|
||||
commit: Added a file with Unicode (日本) in its name and content. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:37:10 +0000 |
||||
fb407a730e3730eb07698f7ced84b596fe7fb7bd -> 30c84cc603759f82470a119c5b5348001ab13500 |
@ -0,0 +1,10 @@ |
||||
--- heads ---------------------------------------------------------------------- |
||||
|
||||
--- tags ----------------------------------------------------------------------- |
||||
|
||||
--- packed refs ---------------------------------------------------------------- |
||||
|
||||
refs/heads/a-branch -> 06d4e9c2beb2c23a955bdb213d02ac63e15e3318 |
||||
refs/heads/master -> 30c84cc603759f82470a119c5b5348001ab13500 |
||||
refs/tags/annotated-tag -> 0214f2698f786600f169378e3001348b345fa3d8 -> 06d4e9c2beb2c23a955bdb213d02ac63e15e3318 |
||||
refs/tags/lightweight-tag -> 30c84cc603759f82470a119c5b5348001ab13500 |
@ -0,0 +1,12 @@ |
||||
--- heads ---------------------------------------------------------------------- |
||||
|
||||
a-branch -> 06d4e9c2beb2c23a955bdb213d02ac63e15e3318 |
||||
master -> 30c84cc603759f82470a119c5b5348001ab13500 |
||||
|
||||
--- tags ----------------------------------------------------------------------- |
||||
|
||||
annotated-tag -> 0214f2698f786600f169378e3001348b345fa3d8 |
||||
lightweight-tag -> 30c84cc603759f82470a119c5b5348001ab13500 |
||||
|
||||
--- packed refs ---------------------------------------------------------------- |
||||
|
Binary file not shown.
@ -0,0 +1,19 @@ |
||||
--- HEAD ----------------------------------------------------------------------- |
||||
|
||||
commit (initial): Added a greeting. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:19:03 +0000 |
||||
-> 2cccb4accf4fffcf9f6487a32b16750dfe298cc7 |
||||
|
||||
commit: Added 2 files in a sub-directory. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:19:33 +0000 |
||||
2cccb4accf4fffcf9f6487a32b16750dfe298cc7 -> 828139d586d32cbe03dc4b32cfefe42e678a3f2e |
||||
|
||||
--- refs/heads/master ---------------------------------------------------------- |
||||
|
||||
commit (initial): Added a greeting. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:19:03 +0000 |
||||
-> 2cccb4accf4fffcf9f6487a32b16750dfe298cc7 |
||||
|
||||
commit: Added 2 files in a sub-directory. |
||||
Taka (tutorial@git-guts) 2021-12-29 03:19:33 +0000 |
||||
2cccb4accf4fffcf9f6487a32b16750dfe298cc7 -> 828139d586d32cbe03dc4b32cfefe42e678a3f2e |
@ -0,0 +1,42 @@ |
||||
--- HEADER --------------------------------------------------------------------- |
||||
|
||||
version: 2 |
||||
entries: 3 |
||||
|
||||
--- ENTRIES -------------------------------------------------------------------- |
||||
|
||||
Entry 0: fpos=0xc |
||||
- path: hello.txt |
||||
- name: f75ba05f340c51065cbea2e1fdbfe5fe13144c97 |
||||
- flags: stage=0, namelen=9 (0x0009) |
||||
- type: regular file (8) |
||||
- size: 14 |
||||
- perms: rw-r--r-- (0x1a4) |
||||
|
||||
Entry 1: fpos=0x54 |
||||
- path: subdir/file1.txt |
||||
- name: 5c1170f2eaac6f78662a8cf899326a4b95c80dd2 |
||||
- flags: stage=0, namelen=16 (0x0010) |
||||
- type: regular file (8) |
||||
- size: 16 |
||||
- perms: rw-r--r-- (0x1a4) |
||||
|
||||
Entry 2: fpos=0xa4 |
||||
- path: subdir/file2.txt |
||||
- name: 3eac351c95c4facb0e99d156f14e3527e0f1c3e0 |
||||
- flags: stage=0, namelen=16 (0x0010) |
||||
- type: regular file (8) |
||||
- size: 16 |
||||
- perms: rw-r--r-- (0x1a4) |
||||
|
||||
--- EXTENSIONS ----------------------------------------------------------------- |
||||
|
||||
TREE: fpos=0xf4, #bytes=56 |
||||
- path = (empty) |
||||
- entries: 3 |
||||
- subtrees: 1 |
||||
- name: 533b8093315fa1071d9cfb2e1543677bea011f6e |
||||
- path = subdir |
||||
- entries: 2 |
||||
- subtrees: 0 |
||||
- name: f2a25c9255b37fb1e9491349524b532a86701bcc |
Loading…
Reference in new issue