Age of git tracked file II

DateReadtime 3 minutes Series Part 2 of Age of git tracked file Tags

Goal

Identify the last time an arbitrary line (let's say line 1) was changed for every file in the repository.

Step 1

Try and use blame to identify changes to the first line only:

$ git blame -L 1,1 -- README
^193d722 (Paul Schwendenman 2012-07-23 16:51:03 +0200 1) ======================

Grab just the commit:

$ git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
  xargs -0n1 git blame -L 1,1 -- | cut -f1 -d" "
f838b75f
e5b94682

Step 2

Keep the file name and the commit id:

$ git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
  xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | paste - - -d, | \
  cut -f1 -d" "
.gitignore,f838b75f
COPYING,e5b94682

Swap the order:

$ git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
  xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | paste - - -d, | \
  cut -f1 -d" " | awk 'BEGIN{FS=",";OFS=",";} {print $2,$1}'
f838b75f,.gitignore
e5b94682,COPYING

Format the output like git arguments:

$  git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
   xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | paste - - -d, | \
  cut -f1 -d" " | awk 'BEGIN{FS=",";OFS=",";} {print $2,$1}' | sed 's/,/ -- /'
f838b75f -- .gitignore
e5b94682 -- COPYING

Step 3

Rebuild the tools from last time:

$  git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
   xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | paste - - -d, | \
   cut -f1 -d" " | awk 'BEGIN{FS=",";OFS=",";} {print $2,$1}' | sed 's/,/ -- /' | \
   xargs -n3 -I{} sh -c 'echo {}; git log -n1 --format="%at:%ar" {}' | paste - - -d,
f838b75f -- .gitignore,1398220033:1 year, 4 months ago
e5b94682 -- COPYING,1347060526:3 years ago

Strip the crap in the front:

$  git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
   xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | paste - - -d, | \
   cut -f1 -d" " | awk 'BEGIN{FS=",";OFS=",";} {print $2,$1}' | sed 's/,/ -- /' | \
   xargs -n3 -I{} sh -c 'echo {}; git log -n1 --format="%at:%ar" {}' | \
   paste - - -d: | cut -f 3- -d" " | sort -k 2 -t:
COPYING:1347060526:3 years ago
.gitignore:1398220033:1 year, 4 months ago

Final formatting:

$  git ls-tree -r HEAD --name-only -z | nul_terminated head -n 2 | \
   xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | paste - - -d, | \
  cut -f1 -d" " | awk 'BEGIN{FS=",";OFS=",";} {print $2,$1}' | sed 's/,/ -- /' | \
  xargs -n3 -I{} sh -c 'echo {}; git log -n1 --format="%at:%ar" {}' | \
  paste - - -d: | cut -f 3- -d" " | sort -k 2 -t: | cut -d: -f1,3- | column -t -s:
COPYING     3 years ago
.gitignore  1 year, 4 months ago

Debugging

Blame puts a ^ in front of the first commit which seems to break everything. That is a but dramatic. It breaks the git log look up and which generates no output. With no output the next command has the wrong number of arguments and that breaks everything. Use sed to remove that.

$  git blame -L 1,1 -- README | sed 's/\^//g'
193d722 (Paul Schwendenman 2012-07-23 16:51:03 +0200 1) ======================

Final

git ls-tree -r HEAD --name-only -z | xargs -0n1 -I{} sh -c 'echo {}; git blame -L 1,1 -- {}' | \
sed 's/\^//g' | paste - - -d, | cut -f1 -d" " | awk 'BEGIN{FS=",";OFS=",";} {print $2,$1}' | \
sed 's/,/ -- /' | xargs -n3 -I{} sh -c 'echo {}; git log -n1 --format="%at:%ar" {}' | \
paste - - -d: | cut -f 3- -d" " | sort -k 2 -t: | cut -d: -f1,3- | column -t -s:

Additional Note

The .gpg files are binary files and by default are unintelligible to git blame. However, you can configure git to use gpg to decrypt the files first.

git config --local blame.gpg.binary true
git config --local blame.gpg.textconv "gpg -d --quiet --yes --compress-algo=none --no-encrypt-to"