← Git: team decisions and recovery
11 / 12 · 60 MIN

Attributes, normalization and checkout

Compare bytes in blobs, the index and disk and identify local policies that change materialization.

Prepare a fixture with known bytes

The runner below creates one temporary repository containing a shell script, a cmd file and a binary payload. The first policy uses -text to retain initial bytes, including CRLF. Script and cmd contents are synthetic; the payload includes null bytes to make representation preservation explicit. The environment ignores global and system configuration, global attributes and personal hooks. It neither reads nor modifies the project repository. Save the complete code as run.py and execute with Python 3.13 and /usr/bin/git 2.50.1 Apple Git-155. The runner checks this version before creating data. Current inspected documentation is from the 2.56 line, but that version was not executed. The first assertion confirms CRLF in the initial blob to establish an observed baseline.

Observe normalization without confusing states

Policy changes to text eol=lf for shell, text eol=crlf for cmd and -text for the binary. The runner stages.gitattributes and runs add --renormalize on tracked paths. It reads staged contents using git show:path: shell and cmd have LF in the index; the binary retains exactly its bytes. The untracked.txt control file remains outside the index. The existing shell file on disk still has CRLF because this operation did not rewrite it. After recording the snapshot, the exercise removes only the two known fixture files and materializes them again from the index. Shell appears with LF and cmd with CRLF. ls-files --eol confirms i/lf and w/crlf for cmd. These states are not contradictory: they are two representations observed at different stages.

Identify effective policy source

The exercise changes eol in working-tree.gitattributes without staging the change. Normal check-attr now reports CRLF for shell while --cached still reports LF. Comparison shows why editor-visible state does not replace an index query. It then restores policy and creates an override in.git/info/attributes: the local value takes precedence again. Removing that override restores the versioned result. In the Mill case, two clones of the same commit can differ because of this local input without any commit corruption. Record effective attributes and their source before copying configuration between a laptop and CI. The laboratory explicitly creates the info directory because its empty template did not create it. This preparation belongs to the isolated fixture, not to changes in personal repositories.

Review the change and consumers

The Quay case mixes normalization with a new timeout. Review functional intent separately from formatting where possible and inspect staged differences before recording. A large diff can hide a small but relevant change. Also test the materialization required by consumers: producing CRLF on a Mac does not establish that the script works on Windows. The byte experiment executes none of these scripts. For handover, deliver policy, version, expected index and destination bytes, binary paths excluded from conversion and outstanding functional tests. The runner continues into release archives in the next lesson and reports 27 checks in total. If an assertion fails, investigate state and version before changing the expected outcome. Editorial review does not replace independent specialist validation or service acceptance.

"""Original DR fixture: attributes, normalization and release archives.
Python 3.13; /usr/bin/git 2.50.1 (Apple Git-155). Run: python3 run.py
Uses one disposable local repository; no network, project files or credentials.
Archives are inspected in memory, never extracted into the filesystem.
"""
import hashlib
import io
import json
import os
from pathlib import Path
import subprocess
import tarfile
import tempfile

GIT = '/usr/bin/git'
version = subprocess.check_output([GIT, '--version'], text=True).strip
assert version == 'git version 2.50.1 (Apple Git-155)', version
checks = []


def check(name, condition):
 assert condition, name
 checks.append(name)


with tempfile.TemporaryDirectory(prefix='dr-git-artifacts-') as directory:
 root = Path(directory)
 repo = root / 'repo'
 hooks = root / 'empty-hooks'
 template = root / 'empty-template'
 hooks.mkdir
 template.mkdir
 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_ATTR_NOSYSTEM='1', GIT_ALLOW_PROTOCOL='file',
 GIT_TERMINAL_PROMPT='0', LC_ALL='C',
 GIT_AUTHOR_NAME='DR Synthetic', GIT_AUTHOR_EMAIL='lab@example.test',
 GIT_COMMITTER_NAME='DR Synthetic', GIT_COMMITTER_EMAIL='lab@example.test',
 GIT_AUTHOR_DATE='2026-10-03T12:00:00+00:00',
 GIT_COMMITTER_DATE='2026-10-03T12:00:00+00:00')
 base = [GIT, '-c', 'core.hooksPath=' + str(hooks), '-c', 'core.attributesFile=' + os.devnull,
 '-c', 'init.templateDir=' + str(template), '-c', 'commit.gpgSign=false',
 '-c', 'core.autocrlf=false', '-c', 'core.fsmonitor=false', '-c', 'tar.umask=0002']

 def run(*args, cwd=None, data=None):
 result = subprocess.run(base + list(args), cwd=cwd or repo, env=env,
 input=data, capture_output=True, timeout=15)
 assert result.returncode == 0, (args, result.stderr.decode)
 return result.stdout

 def text(*args):
 return run(*args).decode.strip

 run('init', '--initial-branch=main', str(repo), cwd=root)
 attrs = repo / '.gitattributes'
 attrs.write_text('*.sh -text\n*.cmd -text\n*.bin -text\n')
 shell = b'#!/bin/sh\necho synthetic\n'
 windows = b'@echo off\r\necho synthetic\r\n'
 binary = b'\x00\xff\r\n\x00payload\r\n'
 (repo / 'deploy.sh').write_bytes(shell.replace(b'\n', b'\r\n'))
 (repo / 'launch.cmd').write_bytes(windows)
 (repo / 'payload.bin').write_bytes(binary)
 run('add', '.')
 run('commit', '-m', 'synthetic initial raw bytes')
 check('baseline: tracked shell blob contains CRLF', b'\r\n' in run('show', 'HEAD:deploy.sh'))
 policy = '*.sh text eol=lf\n*.cmd text eol=crlf\n*.bin -text\ndocs/** export-ignore\nVERSION export-subst\n'
 attrs.write_text(policy)
 (repo / 'untracked.txt').write_text('not part of release\n')
 run('add', '.gitattributes')
 run('add', '--renormalize', '.')
 check('normalization: shell index is LF', run('show', ':deploy.sh') == shell)
 check('normalization: cmd index is LF', run('show', ':launch.cmd') == windows.replace(b'\r\n', b'\n'))
 check('normalization: binary index preserves exact bytes', run('show', ':payload.bin') == binary)
 check('normalization: renormalize does not add untracked file', text('ls-files', '--', 'untracked.txt') == '')
 check('normalization: add does not rewrite existing shell worktree bytes', b'\r\n' in (repo / 'deploy.sh').read_bytes)
 run('commit', '-m', 'normalize tracked fixture')
 # Remove only known fixture paths to force a fresh materialization from index.
 for name in ('deploy.sh', 'launch.cmd'):
 (repo / name).unlink
 run('restore', '--worktree', '--', 'deploy.sh', 'launch.cmd')
 check('checkout: shell materializes with LF', (repo / 'deploy.sh').read_bytes == shell)
 check('checkout: cmd materializes with CRLF', (repo / 'launch.cmd').read_bytes == windows)
 check('checkout: ls-files separates index LF and worktree CRLF',
 'i/lf' in text('ls-files', '--eol', 'launch.cmd') and 'w/crlf' in text('ls-files', '--eol', 'launch.cmd'))
 attrs.write_text(policy.replace('*.sh text eol=lf', '*.sh text eol=crlf'))
 check('attributes: working-tree policy reports CRLF', text('check-attr', 'eol', '--', 'deploy.sh').endswith(': crlf'))
 check('attributes: cached policy still reports LF', text('check-attr', '--cached', 'eol', '--', 'deploy.sh').endswith(': lf'))
 attrs.write_text(policy)
 info = repo / '.git' / 'info' / 'attributes'
 info.parent.mkdir(exist_ok=True)
 info.write_text('deploy.sh eol=crlf\n')
 check('attributes: local info override takes precedence', text('check-attr', 'eol', '--', 'deploy.sh').endswith(': crlf'))
 info.unlink
 check('attributes: removing local override restores versioned policy', text('check-attr', 'eol', '--', 'deploy.sh').endswith(': lf'))
 (repo / 'docs').mkdir
 (repo / 'docs' / 'internal.txt').write_text('synthetic internal notes\n')
 (repo / 'VERSION').write_text('commit=$Format:%H$\n')
 (repo / 'current').symlink_to('deploy.sh')
 run('add', 'docs/internal.txt', 'VERSION', 'current')
 run('update-index', '--chmod=+x', 'deploy.sh')
 run('commit', '-m', 'synthetic release contents')
 commit_id = text('rev-parse', 'HEAD')
 check('release: index records executable shell mode', text('ls-files', '--stage', 'deploy.sh').startswith('100755 '))
 (repo / 'deploy.sh').write_bytes(b'local draft not released\n')
 archive = run('archive', '--format=tar', '--prefix=release/', commit_id)
 archive_again = run('archive', '--format=tar', '--prefix=release/', commit_id)
 check('archive: repeated same-commit local tar bytes match', archive == archive_again)
 with tarfile.open(fileobj=io.BytesIO(archive), mode='r:') as tar:
 names = tar.getnames
 read = lambda name: tar.extractfile('release/' + name).read
 check('archive: committed shell content excludes local draft', read('deploy.sh') == shell)
 check('archive: untracked file is absent', 'release/untracked.txt' not in names)
 check('archive: export-ignore omits tracked internal document', 'release/docs/internal.txt' not in names)
 check('archive: export-subst writes selected commit identity', read('VERSION') == ('commit=' + commit_id + '\n').encode)
 check('archive: shell entry retains executable bits', bool(tar.getmember('release/deploy.sh').mode & 0o111))
 member = tar.getmember('release/current')
 check('archive: symlink remains a link with its target', member.issym and member.linkname == 'deploy.sh')
 check('archive: tar metadata reports selected commit ID', run('get-tar-commit-id', data=archive).decode.strip == commit_id)
 tree_archive = run('archive', '--format=tar', commit_id + '^{tree}')
 with tarfile.open(fileobj=io.BytesIO(tree_archive), mode='r:') as tar:
 check('archive: a tree archive leaves export-subst placeholder', tar.extractfile('VERSION').read == b'commit=$Format:%H$\n')
 attrs.write_text(policy.replace('docs/** export-ignore\n', ''))
 default_archive = run('archive', '--format=tar', commit_id)
 worktree_archive = run('archive', '--format=tar', '--worktree-attributes', commit_id)
 with tarfile.open(fileobj=io.BytesIO(default_archive), mode='r:') as tar:
 check('archive: default uses committed exclusion despite edited worktree policy', 'docs/internal.txt' not in tar.getnames)
 with tarfile.open(fileobj=io.BytesIO(worktree_archive), mode='r:') as tar:
 check('archive: explicit worktree attributes alter archive membership', 'docs/internal.txt' in tar.getnames)
 check('archive: worktree attributes do not substitute working-tree file content', tar.extractfile('deploy.sh').read == shell)
 check('archive: source commit still contains excluded document', run('show', commit_id + ':docs/internal.txt') == b'synthetic internal notes\n')

print(json.dumps({'version': version, 'checks_passed': len(checks), 'checks': checks,
 'scope': 'One disposable local repository; byte, index, checkout and in-memory tar checks. '
 'No production build, deployment, network, signing, Windows runtime or Git 2.56 execution.',
 'sourceSha256': hashlib.sha256(Path(__file__).read_bytes).hexdigest}, indent=2))
IN PRACTICE

The launch.cmd index contains LF while its fresh checkout contains CRLF; payload.bin retains original bytes.

Common pitfalls

Confusing eol with encoding; mixing logic with normalization; converting binaries; copying personal overrides into CI without review.

Related topics: States and index · Diagnosis and releases · Integration and review

Take this idea with you

Define expected bytes and policy at each stage, then verify materialization and consumer behavior.

Create account

Reference: Git manual and original DR artifact experiments · Git 2.56; workflow concepts compatible with modern Git 2.x