{"thread":{"id":"25213","subject":"Listing files in order they are changed","startedAt":"2010-09-23T18:51:56Z","lastAt":"2010-09-24T07:59:19Z","messageCount":7,"participants":["Parag Kalra","Jeff King","Jakub Narebski","Sverre Rabbelier","Peter Krefting"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"151386","messageId":"AANLkTimHrCujEggj9KbAWWivY+WxnL0YqsXs+idhymBA@mail.gmail.com","threadId":"25213","inReplyTo":null,"subject":"Listing files in order they are changed","fromName":"Parag Kalra","fromEmail":"paragkalra@gmail.com","sentAt":"2010-09-23T18:51:56Z","receivedAt":"2010-09-23T18:51:56Z","isPatch":false,"sender":{"key":"paragkalra@gmail.com","avatar":null},"body":"Hi All,\n\nIs there a way I can make Git list all the tracked files in the\nworking directory in order they are changed (modified, added or\ndeleted)\n\nSomething like displayed by: git ls-files\n\nCheers,\nParag\n"},{"id":"151395","messageId":"20100923203814.GA10951@sigill.intra.peff.net","threadId":"25213","inReplyTo":"AANLkTimHrCujEggj9KbAWWivY+WxnL0YqsXs+idhymBA@mail.gmail.com","subject":"Re: Listing files in order they are changed","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2010-09-23T20:38:15Z","receivedAt":"2010-09-23T20:38:15Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Sep 23, 2010 at 11:51:56AM -0700, Parag Kalra wrote:\n\n> Is there a way I can make Git list all the tracked files in the\n> working directory in order they are changed (modified, added or\n> deleted)\n\nIt's a somewhat expensive operation, since you have to walk history\nbackwards looking at changes. But try:\n\ngit log --pretty=format: --name-only |\n  grep . |\n  perl -ne 'print unless $seen{$_}++'\n\nwhich will walk history backwards, printing out each filename the most\nrecent time it is encountered. Which should give you a list of all files\nin the repo (including ones which no longer exist!), in order of most\nrecent to oldest.\n\nIf you want to restrict only to existing files, you would have to do\nsome clever path-limiting, or parse the results with a slightly smarter\nperl script.\n\nYou can also tweak the definition of \"most recent\" by using --topo-order\nor --date-order with git log.\n\n-Peff\n"},{"id":"151397","messageId":"m3pqw4gpb2.fsf@localhost.localdomain","threadId":"25213","inReplyTo":"AANLkTimHrCujEggj9KbAWWivY+WxnL0YqsXs+idhymBA@mail.gmail.com","subject":"Re: Listing files in order they are changed","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-09-23T20:48:07Z","receivedAt":"2010-09-23T20:48:07Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Parag Kalra <paragkalra@gmail.com> writes:\n\n> Is there a way I can make Git list all the tracked files in the\n> working directory in order they are changed (modified, added or\n> deleted)\n> \n> Something like displayed by: git ls-files\n\nNo, it is currently not possible, mainly because git thinks in terms\nof changes to project as a whole, not as changes to individual files.\n\nThe tree blame (\"git blame <directory>\"), if it existed (I have\nworking prototype), would be of use here.\n-- \nJakub Narebski\nPoland\nShadeHawk on #git\n"},{"id":"151399","messageId":"AANLkTi=_GOBuha0cw=qbUTXyUMdiLANPZXoPSOs_rKT8@mail.gmail.com","threadId":"25213","inReplyTo":"m3pqw4gpb2.fsf@localhost.localdomain","subject":"Re: Listing files in order they are changed","fromName":"Sverre Rabbelier","fromEmail":"srabbelier@gmail.com","sentAt":"2010-09-23T20:53:07Z","receivedAt":"2010-09-23T20:53:07Z","isPatch":false,"sender":{"key":"srabbelier@gmail.com","avatar":"https://avatars.githubusercontent.com/u/3098?v=4"},"body":"Heya,\n\nOn Thu, Sep 23, 2010 at 22:48, Jakub Narebski <jnareb@gmail.com> wrote:\n> The tree blame (\"git blame <directory>\"), if it existed (I have\n> working prototype), would be of use here.\n\nOk, I'll bite. Won't you please show us your tree blame prototype Jakub? :P\n\n-- \nCheers,\n\nSverre Rabbelier\n"},{"id":"151400","messageId":"201009232302.14925.jnareb@gmail.com","threadId":"25213","inReplyTo":"AANLkTi=_GOBuha0cw=qbUTXyUMdiLANPZXoPSOs_rKT8@mail.gmail.com","subject":"Re: Listing files in order they are changed","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-09-23T21:02:13Z","receivedAt":"2010-09-23T21:02:13Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 23 Sep 2010, Sverre Rabbelier wrote:\n> On Thu, Sep 23, 2010 at 22:48, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> > The tree blame (\"git blame <directory>\"), if it existed (I have\n> > working prototype), would be of use here.\n> \n> Ok, I'll bite. Won't you please show us your tree blame prototype Jakub? :P\n\nI think it works correctly now also for directories other than top one.\nAvert your eyes, ugly code ahead ;-) (and code copied from gitweb.perl\nand Git.pm / Git/Repo.pm)  \n\n-- >8 --\n#!/usr/bin/perl\n\nuse strict;\nuse warnings;\nuse Data::Dumper;\n\nuse Encode;\nuse Fcntl ':mode';\nuse List::Compare::Functional qw(:originals);\nuse List::MoreUtils qw(uniq pairwise);\n\nuse constant DEBUG => 0;\n\n# ----------------------------------------------------------------------\n{\npackage Git::Repo;\n\nuse strict;\nuse warnings;\n\nuse IPC::Open2 qw(open2);\nuse IO::Handle;\n\nuse base qw(Exporter);\n\nour @EXPORT = qw();\nour @EXPORT_OK = qw();\n\n# Auxiliary subroutines\n\nsub _assert_opts {\n\tdie \"must have an even number of arguments for named options\"\n\t\tunless $#_ % 2;\n}\n\nsub _assert_sha1 {\n\tmy $sha1 = shift;\n\tdie \"'$sha1' is not a SHA1 (need to use get_sha1?)\"\n\t\tunless $sha1 && $sha1 =~ /^[a-f0-9]{40}$/;\n}\n\nsub new {\n\tmy $class = shift;\n\t_assert_opts @_;\n\tmy $self = {@_};\n\tbless $self, $class;\n\tdie 'no repo_dir given' unless $self->{repo_dir};\n\treturn $self;\n}\n\n# Return the first items of the git command line, for instance\n# qw(/usr/bin/git --git-dir=/path/to/repo.git).\nsub _git_cmd {\n\tmy $self = shift;\n\treturn ($self->{git_binary} || 'git', '--git-dir=' . $self->{repo_dir});\n}\n\nsub get_sha1 {\n\tmy ($self, $object_id) = @_;\n\n\tdie 'no object identifier given' unless $object_id;\n\tdie 'object identifier must not contain newlines' if $object_id =~ /\\n/;\n\n\tunless ($self->{sha1_stdout}) {\n\t\t# Open bidi pipe the first time get_sha1 is called.\n\t\t# open2 raises an exception on error, no need to 'or die'.\n\t\t$self->{sha1_pid} =\n\t\t\topen2($self->{sha1_stdout}, $self->{sha1_stdin},\n\t\t\t      $self->_git_cmd, 'cat-file', '--batch-check');\n\t}\n\t$self->{sha1_stdin}->printflush(\"$object_id\\n\")\n\t\tor die \"cannot write to pipe: $!\";\n\tmy $output = $self->{sha1_stdout}->getline()\n\t\tor die \"cannot read from pipe: $!\";\n\tchomp $output;\n\treturn if $output =~ /missing$/;\n\tmy ($sha1, $type, $size) =\n\t\t($output =~ /^([0-9a-f]{40}) ([a-z]+) ([0-9]+)$/)\n\t\tor die \"invalid response: $output\";\n\treturn wantarray ? ($sha1, $type, $size) : $sha1;\n}\n\n\nsub get_object {\n\tmy ($self, $object_id) = @_;\n\n\tunless ($self->{object_stdout}) {\n\t\t# Open bidi pipe the first time get_object is called.\n\t\t# open2 raises an exception on error, no need to 'or die'.\n\t\t$self->{object_pid} =\n\t\t\topen2($self->{object_stdout}, $self->{object_stdin},\n\t\t\t      $self->_git_cmd, 'cat-file', '--batch');\n\t}\n\t$self->{object_stdin}->printflush(\"$object_id\\n\")\n\t\tor die \"get_object: cannot write to pipe: $!\";\n\tmy ($sha1, $type, $size) =\n\t\tsplit ' ', $self->{object_stdout}->getline()\n\t\tor die \"get_object: cannot read from pipe: $!\";\n\tdie \"'$object_id' not found in repository\"\n\t\tif $type eq 'missing';\n\t$self->{object_stdout}->read(my $content, $size);\n\t$self->{object_stdout}->getline();  # eat trailing newline\n\treturn wantarray ? ($sha1, $type, $size, $content) : $content;\n}\n\nsub get_commit_difftree {\n\tmy ($self, $commit_id, $parent_id, $path) = @_;\n\n\t$path =~ s!([^/])$!$1/!; # add trailing '/' if it is not present\n\tunless ($self->{difftree_stdout}) {\n\t\t# Open bidi pipe the first time get_object is called.\n\t\t# open2 raises an exception on error, no need to 'or die'.\n\t\t$self->{difftree_pid} =\n\t\t\topen2($self->{difftree_stdout}, $self->{difftree_stdin},\n\t\t\t      $self->_git_cmd, 'diff-tree', '--stdin', '--raw', '--no-commit-id',\n\t\t\t                       '--root', '--no-renames',\n\t\t\t#                       defined $path ? ('--', $path) : ());\n\t\t\t                       defined $path ? (\"--relative=$path\", \"-t\") : ());\n\t}\n\t# the additional LF (\"\\n\") is to able to detect end of difftree\n\t$self->{difftree_stdin}->printflush(\"$commit_id $parent_id\\n\\n\")\n\t\tor die \"get_commit_difftree: cannot write to pipe: $!\";\n\n\tmy @difftree_lines;\n\twhile (my $line = $self->{difftree_stdout}->getline()) {\n\t\tchomp $line;\n\t\tlast unless $line;\n\n\t\tpush @difftree_lines, $line;\n\t}\n\treturn wantarray ? @difftree_lines : \\@difftree_lines;\n}\n\n\nsub DESTROY {\n\tmy $self = shift;\n\n\tif (exists $self->{object_stdout}) {\n\t\tclose $self->{object_stdout}\n\t\t\tor die \"Closing stdout of git-cat-file --batch failed: $!\";\n\t}\n\tif (exists $self->{object_stdin}) {\n\t\tclose $self->{object_stdin}\n\t\t\tor die \"Closing stdin of git-cat-file --batch  failed: $!\";\n\t}\n\tif (exists $self->{object_pid}) {\n\t\twaitpid $self->{object_pid}, 0\n\t\t\tor die \"Waiting for pid=$self->{object_pid} failed: $!\";\n\t}\n\n\tif (exists $self->{sha1_stdout}) {\n\t\tclose $self->{sha1_stdout}\n\t\t\tor die \"Closing stdout of git-cat-file --batch-check failed: $!\";\n\t}\n\tif (exists $self->{sha1_stdin}) {\n\t\tclose $self->{sha1_stdin}\n\t\t\tor die \"Closing stdin of git-cat-file --batch-check failed: $!\";\n\t}\n\tif (exists $self->{sha1_pid}) {\n\t\twaitpid $self->{sha1_pid}, 0\n\t\t\tor die \"Waiting for pid=$self->{sha1_pid} failed: $!\";\n\t}\n\n\tif (exists $self->{difftree_stdout}) {\n\t\tclose $self->{difftree_stdout}\n\t\t\tor die \"Closing stdout of git-cat-file --batch-check failed: $!\";\n\t}\n\tif (exists $self->{difftree_stdin}) {\n\t\tclose $self->{difftree_stdin}\n\t\t\tor die \"Closing stdin of git-cat-file --batch-check failed: $!\";\n\t}\n\tif (exists $self->{difftree_pid}) {\n\t\twaitpid $self->{difftree_pid}, 0\n\t\t\tor die \"Waiting for pid=$self->{difftree_pid} failed: $!\";\n\t}\n}\n\n} # end package Git::Repo;\n\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nsub set_signals {\n\tmy $pid = shift;\n\n\t$SIG{'PIPE'} = 'IGNORE';\n\t$SIG{'CHLD'} = 'IGNORE';\n\n\t$SIG{'CHLD'} = sub {\n\t\tprint \"REAPER: status $? on $pid\\n\" \n\t\t\tif waitpid($pid, 0) > 0;\n\t};\n}\n\nsub cat_file_gitrepo {\n\tmy ($repo, $object_name) = @_;\n\n\tmy ($ret_sha1, $type, $size, $content) =\n\t\t$repo->get_object($object_name);\n\treturn wantarray ? (\"$ret_sha1 $type $size\", $content) : $content;\n}\n\n# ----------------------------------------------------------------------\n\nmy $fallback_encoding = 'latin1';\n# decode sequences of octets in utf8 into Perl's internal form,\n# which is utf-8 with utf8 flag set if needed.  gitweb writes out\n# in utf-8 thanks to \"binmode STDOUT, ':utf8'\" at beginning\nsub to_utf8 {\n\tmy $str = shift;\n\tif (utf8::valid($str)) {\n\t\tutf8::decode($str);\n\t\treturn $str;\n\t} else {\n\t\treturn decode($fallback_encoding, $str, Encode::FB_DEFAULT);\n\t}\n}\n\nsub parse_commit {\n\tmy ($commit_text) = @_;\n\tmy @commit_lines = split '\\n', $commit_text;\n\tmy %co;\n\n\tif (! @commit_lines) {\n\t\treturn;\n\t}\n\n\tmy @parents;\n HEADER:\n\twhile (my $line = shift @commit_lines) {\n\t\tlast if $line eq \"\\n\";\n\t\tif ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {\n\t\t\t$co{'tree'} = $1;\n\t\t} elsif ($line =~ m/^parent ([0-9a-fA-F]{40})$/) {\n\t\t\tpush @parents, $1;\n\t\t} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {\n\t\t\t$co{'author'} = to_utf8($1);\n\t\t\t$co{'author_epoch'} = $2;\n\t\t\t$co{'author_tz'} = $3;\n\t\t\tif ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {\n\t\t\t\t$co{'author_name'}  = $1;\n\t\t\t\t$co{'author_email'} = $2;\n\t\t\t} else {\n\t\t\t\t$co{'author_name'} = $co{'author'};\n\t\t\t}\n\t\t} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {\n\t\t\t$co{'committer'} = to_utf8($1);\n\t\t\t$co{'committer_epoch'} = $2;\n\t\t\t$co{'committer_tz'} = $3;\n\t\t\tif ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {\n\t\t\t\t$co{'committer_name'}  = $1;\n\t\t\t\t$co{'committer_email'} = $2;\n\t\t\t} else {\n\t\t\t\t$co{'committer_name'} = $co{'committer'};\n\t\t\t}\n\t\t}\n\t}\n\tif (!defined $co{'tree'}) {\n\t\treturn;\n\t};\n\t$co{'parents'} = \\@parents;\n\t$co{'parent'}  =  $parents[0];\n\n\t$co{'comment'} = \\@commit_lines;\n\n\treturn wantarray ? %co : \\%co;\n}\n\nsub unquote { return $_[0] }\n\n# parse line of git-ls-tree output\nsub parse_ls_tree_line {\n\tmy $line = shift;\n\tmy %opts = @_;\n\tmy %res;\n\n\t#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa\tpanic.c'\n\t$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\\t(.+)$/s;\n\n\t$res{'mode'} = $1;\n\t$res{'type'} = $2;\n\t$res{'hash'} = $3;\n\tif ($opts{'-z'}) {\n\t\t$res{'name'} = $4;\n\t} else {\n\t\t$res{'name'} = unquote($4);\n\t}\n\n\treturn wantarray ? %res : \\%res;\n}\n\n# parse line of git-diff-tree \"raw\" output\nsub parse_difftree_raw_line {\n\tmy $line = shift;\n\tmy %res;\n\n\t# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M\tls-files.c'\n\t# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M\trev-tree.c'\n\tif ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\\t(.*)$/) {\n\t\t$res{'from_mode'} = $1;\n\t\t$res{'to_mode'} = $2;\n\t\t$res{'from_id'} = $3;\n\t\t$res{'to_id'} = $4;\n\t\t$res{'status'} = $5;\n\t\t$res{'similarity'} = $6;\n\t\tif ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied\n\t\t\t($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split(\"\\t\", $7);\n\t\t} else {\n\t\t\t$res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);\n\t\t}\n\t}\n\t# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR\tgit-gui/git-gui.sh'\n\t# combined diff (for merge commit)\n\telsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\\t(.*)$//) {\n\t\t$res{'nparents'}  = length($1);\n\t\t$res{'from_mode'} = [ split(' ', $2) ];\n\t\t$res{'to_mode'} = pop @{$res{'from_mode'}};\n\t\t$res{'from_id'} = [ split(' ', $3) ];\n\t\t$res{'to_id'} = pop @{$res{'from_id'}};\n\t\t$res{'status'} = [ split('', $4) ];\n\t\t$res{'to_file'} = unquote($5);\n\t}\n\t# 'c512b523472485aef4fff9e57b229d9d243c967f'\n\telsif ($line =~ m/^([0-9a-fA-F]{40})$/) {\n\t\t$res{'commit'} = $1;\n\t}\n\n\treturn wantarray ? %res : \\%res;\n}\n\n# ......................................................................\n\n# parse one entry of raw 'tree' object output (from 'git cat-file --batch')\nsub decode_tree_entry {\n\tmy $buf = shift;\n\t#use bytes;\n\n\t$buf =~ s/^([0-7]+) //;\n\tmy ($mode_str) = $1;\n\tmy ($filename, $sha1_str) = unpack('Z*H[40]', $buf);\n\n\treturn ($mode_str, $filename, $sha1_str);\n}\n\nsub tree_entry_len {\n\tmy ($mode_str, $filename) = @_;\n\t#use bytes;\n\n\t# length of mode string + separator + 20 bytes of SHA-1\n\t# + length of filename (in bytes) + terminating NUL ('\\0')\n\tlength($mode_str)+1 + length($filename)+1 + 20;\n}\n\nuse constant {\n\tS_IFINVALID => 0030000,\n\tS_IFGITLINK => 0160000,\n};\n\n# submodule/subproject, a commit object reference\nsub S_ISGITLINK {\n\tmy $mode = shift;\n\n\treturn (($mode & S_IFMT) == S_IFGITLINK)\n}\n\nsub type_from_mode {\n\tmy $mode = oct shift;\n\n\tif (S_ISGITLINK($mode)) {\n\t\treturn \"commit\";\n\t} elsif (S_ISDIR($mode & S_IFMT)) {\n\t\treturn \"tree\";\n\t}\n\t\treturn \"blob\";\n}\n\n# ......................................................................\n\nsub decode_tree {\n\tmy $contents = shift;\n\t#use bytes;\n\n\tmy @result;\n\n\twhile (my @entry = decode_tree_entry($contents)) {\n\t\t#print join(' ', @entry).\"\\n\";\n\t\t#printf(\"%06d %s\\t%s\\n\", $entry[0], $entry[2], $entry[1]);\n\n\t\tpush @result,\n\t\t\t{ 'mode' => $entry[0],\n\t\t\t  'type' => type_from_mode($entry[0]),\n\t\t\t  'name' => $entry[1],\n\t\t\t  'hash' => $entry[2] };\n\n\t\tmy $len = tree_entry_len(@entry);\n\t\t#print substr($contents, 0, $len).\"\\n\";\n\t\t$contents = substr($contents, $len);\n\t\tlast unless $contents;\n\t}\n\n\treturn wantarray ? @result : \\@result;\n}\n\nsub tree_entry_eq {\n\tmy ($a, $b) = @_;\n\n\treturn\n\t\t$a->{'mode'} == $b->{'mode'} &&\n\t\t$a->{'type'} eq $b->{'type'} &&\n\t\t$a->{'name'} eq $b->{'name'} &&\n\t\t$a->{'hash'} eq $b->{'hash'};\n}\n\n# ......................................................................\n\nsub print_parsed_tree {\n\tmy $tree = shift;\n\n\tforeach my $tree_entry (@$tree) {\n\t\tprint format_tree_entry($tree_entry).\"\\n\";\n\t}\n}\n\nsub print_tree_blame {\n\tmy ($tree, $tree_blame) = @_;\n\n\tforeach my $tree_entry (@$tree) {\n\t\t#print format_tree_blame_entry($tree_blame->{$tree_entry->{'hash'}}).\"\\n\";\n\t\tprint format_tree_blame_entry($tree_entry).\"\\n\";\n\t}\n}\n\nsub format_tree_entry {\n\tmy $tree_entry = shift;\n\n\treturn sprintf(\"%06d %s %s\\t%s\",\n\t\t$tree_entry->{'mode'}, $tree_entry->{'type'},\n\t\t$tree_entry->{'hash'}, $tree_entry->{'name'});\n}\n\nsub format_tree_blame_entry {\n\tmy $tree_entry = shift;\n\n\t#my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday)\n\t#\t= gmtime($tree_entry->{'author_epoch'});\n\t#return sprintf(\"%06d %s %.8s %.8s %.15s %02d-%02d-%04d\\t%s\",\n\t#\t$tree_entry->{'mode'}, $tree_entry->{'type'},\n\t#\t$tree_entry->{'hash'}, $tree_entry->{'commit'},\n\t#\t$tree_entry->{'author_name'}, $mday, $mon, 1900 + $year,\n\t#\t$tree_entry->{'name'});\n\treturn sprintf(\"%06d %s %.8s %.8s\\t%.10s\\t%s\",\n\t\t$tree_entry->{'mode'}, $tree_entry->{'type'},\n\t\t$tree_entry->{'hash'}, $tree_entry->{'commit'},\n\t\t$tree_entry->{'name'}, $tree_entry->{'summary'});\n}\n\n# ======================================================================\n\nsub tree_blame_commit {\n\tmy ($repo, $commit_id, $tree_blame, $path) = @_;\n\n\tmy ($commit_sha1, undef, undef, $commit_text) =\n\t\t$repo->get_object($commit_id);\n\tmy %commit = parse_commit($commit_text);\n\n\tmy $nunblamed = scalar grep { !exists $_->{'commit'} } @$tree_blame;\n\tprintf(\"processing %6s (%1d parents, %d unblamed): %s\\n\",\n\t       substr($commit_sha1,0,6), scalar @{$commit{'parents'}},\n\t       $nunblamed, $commit{'comment'}[0])\n\t\tif DEBUG >= 1;\n\tprint Dumper($tree_blame)\n\t\tif DEBUG >= 2;\n\treturn unless $nunblamed > 0;\n\n\tforeach my $parent (@{$commit{'parents'}}) {\n\t\tmy @difftree = $repo->get_commit_difftree($commit_sha1, $parent, $path);\n\t\t#my @difftree = $repo->get_commit_difftree(\"$commit_sha1:$path\", \"$parent:$path\");\n\t\tif (DEBUG >= 1) {\n\t\t\tprint \"$_\\n\" foreach (@difftree);\n\t\t}\n\t\t@difftree = map { { parse_difftree_raw_line($_) } } @difftree;\n\t\t# work around current deficiences of '--relative[=<path>]'\n\t\t# which require for this script to use '-t' (recursive mode)\n\t\t@difftree = grep { index($_->{'to_file'}, '/') == -1 } @difftree\n\t\t\tif (defined $path);\n\n\t\tprintf(\"processing %6s: parent %6s has %d in difftree\\n\",\n\t\t       substr($commit_sha1,0,6), substr($parent,0,6),\n\t\t       scalar @difftree)\n\t\t\tif DEBUG >= 1;\n\n\t\tno warnings 'recursion';\n\n\t\t# mark entries from @difftree\n\t\t#mark_changed($tree_blame, \\@difftree, [ $commit_sha1 ]);\n\t\tmark_changed($tree_blame, \\@difftree, \\$commit_sha1);\n\t\t# pass blame to parent\n\t\ttree_blame_commit($repo, $parent, $tree_blame, $path);\n\t\t# remove marks\n\t\t#remove_marks($tree_blame, [ $commit_sha1 ]);\n\t\tremove_marks($tree_blame, \\$commit_sha1);\n\t}\n\n\tmy $nblames_this = 0;\n\tforeach my $tree_entry (@$tree_blame) {\n\t\tif (!exists $tree_entry->{'commit'}) {\n\t\t\t$tree_entry->{'commit'}  = $commit_sha1;\n\t\t\t$tree_entry->{'summary'} = $commit{'comment'}[0];\n\t\t\t$tree_entry->{'author_name'} = $commit{'author_name'};\n\t\t\tif ($commit{'author_email'} &&\n\t\t\t    $commit{'author_email'} =~ /^([^@]+)@/) {\n\t\t\t\t$tree_entry->{'author_user'} = $1;\n\t\t\t}\n\t\t\t$tree_entry->{'author_epoch'} = $commit{'author_epoch'};\n\n\t\t\tmy @difftree = $repo->get_commit_difftree($commit_sha1, '-m', $path);\n\t\t\t@difftree = map { { parse_difftree_raw_line($_) } } @difftree;\n\t\t\t$tree_entry->{'difftree'} =\n\t\t\t\t[ grep { $_->{'to_id'} eq $tree_entry->{'hash'} } @difftree ];\n\n\t\t\t$nblames_this++;\n\t\t}\n\t}\n\n\tprintf(\"done %6s (got blamed by %1d, %d unblamed left)\\n\",\n\t       substr($commit_sha1,0,6), $nblames_this,\n\t       scalar grep { !exists $_->{'commit'} } @$tree_blame)\n\t\tif DEBUG >= 1;\n}\n\nsub mark_changed {\n\tmy ($tree_blame, $difftree, $value) = @_;\n\n\tmy @blame_sha1    = map { $_->{'hash'} }  @$tree_blame;\n\tmy @difftree_sha1 = map { $_->{'to_id'} } @$difftree;\n\t# not optimized: both @$tree_blame and @$difftree are sorted by filename\n\tmy @common_sha1 = get_intersection('--unsorted', [ \\@blame_sha1, \\@difftree_sha1 ]);\n\n\tmy %tree_blame = map { $_->{'hash'} => $_ } @$tree_blame;\n\tmap { $_->{'commit'} = $value unless exists $_->{'commit'} }\n\t\t@tree_blame{@common_sha1};\n\t@$tree_blame = values %tree_blame;\n\n\treturn @$tree_blame;\n}\n\nsub remove_marks {\n\tmy ($tree_blame, $value) = @_;\n\n\tmap {\n\t\tdelete($_->{'commit'})\n\t\t\t#if (ref($_->{'commit'}) && ($_->{'commit'}->[0] eq $value->[0]))\n\t\t\tif (ref($_->{'commit'}) && (${$_->{'commit'}} eq $$value))\n\t}\t@$tree_blame;\n\n\treturn @$tree_blame;\n}\n\n# ======================================================================\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# ----------------------------------------------------------------------\n# MAIN\n\nmy $git_dir = \"/home/jnareb/git/gitweb/test/.git\";\nmy $tree_path    = \"sub\";\n#my $tree_path    = \"\";\nmy $start_commit = \"HEAD\";\n\n#my $git_dir = \"/home/jnareb/git/.git\";\n#my $tree_path = \"contrib\";\n#my $start_commit = \"HEAD\";\n#my $tree_path    = '';\n#my $start_commit = \"todo\";\n\nmy $repo = Git::Repo->new(repo_dir=>$git_dir);\n\nmy ($sha1, $type, $size, $obj_data) =\n\t$repo->get_object(\"$start_commit:$tree_path\");\nmy @tree_blame = decode_tree($obj_data);\n\ntree_blame_commit($repo, $start_commit, \\@tree_blame, $tree_path);\n\nprint Dumper(\\@tree_blame)\n\tif DEBUG >= 2;\n\nprint_tree_blame(\\@tree_blame)\n\tif DEBUG < 2;\n\n#print Dumper(\\@tree_blame);\n#print Dumper($tree_blame[0]);\n\n__END__\n# ----------------------------------------------------------------------\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# ======================================================================\n\n#http://git.or.cz/gitwiki/ExampleScripts#Findingwhichcommitslasttouchedthefiles\n#http://gist.github.com/247395\n\nmy %attributions = ();\nmy @files = ();\n\nopen IN, \"git ls-tree -r --full-name HEAD |\" or die;\nwhile (<IN>) {\n\tif (/^\\S+\\s+blob \\S+\\s+(\\S+)$/) {\n\t\t$files[$#files + 1] = $1;\n\t\t$attributions{$1} = -1;\n\t}\n}\nclose IN;\n\nmy $remaining = $#files + 1;\n\nopen IN, \"git log -r --root --raw --no-abbrev --pretty=format:%h~%an~%ad~ |\" or die;\nwhile (<IN>) {\n\tif (/^([^:~]+)~(.*)~([^~]+)~$/) {\n\t\t($commit, $author, $date) = ($1, $2, $3);\n\t} elsif (/^:\\S+\\s+1\\S+\\s+\\S+\\s+\\S+\\s+\\S\\s+(.*)$/) {\n\t\tif ($attributions{$1} == -1) {\n\t\t\t$attributions{$1} = \"$author, $date ($commit)\";\n\t\t\t$remaining--;\n\t\t\tif ($remaining <= 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\nclose IN;\n\nfor $f (@files) {\n\tprint \"$f\t$attributions{$f}\\n\";\n}\n\n# ######################################################################\n# ######################################################################\n__DATA__\nCurrently git-blame supports only ordinary files (blobs).  If you try to \nuse git-blame on directory, it errors-out with a bit cryptic error \nmessage:\n\n  $ git blame Documentation\n  fatal: unsupported file type Documentation\n\nMeanwhile some git web interface (e.g. GitHub and Gitorious), most \nprobably following web interfaces for file-based VCS like ViewVC for \nCVS and Subversion, provide kind of \"tree blame\" view as default view \nfor directory contents.  It means that for each element (entry) in \ngiven directory it shows 'last changed' info, namely author, date and \nsummary of commit that changed given entry to current version.\n\nI don't know what algorithm they use to generate this info (well,\nI could find out in the case of Gitorious... if I read Ruby ;-)), \nbut I suspect that they might a bit inefficient algorithm to find such \ninfo.  Some time ago I tried to add such 'tree_blame' view to gitweb:\nyou can check the result in the 'gitweb/tree_blame' branch in my \ngit/jnareb-git.git repository at repo.or.cz:\n  http://repo.or.cz/w/git/jnareb-git.git?a=commitdiff;h=gitweb/tree_blame\n\n\nIt would be nice if \"git blame <directory>\" would give us required \ninformation; for tools such like GitHub, Gitorious or gitweb one could \nuse '--porcelain' or '--incremental' output.\n\nUnfortunately I don't know this part of code good enough to write it\neasily myself. I would think that it wouldn't be too hard to code it;\ncertainly easier than git-blame for ordinary files.\n\nI think that ordinary git-blame output for trees (directories) could\nmimic \"ls -l\" output format as far as possible, i.e. when currently\n  $ git ls-tree --abbrev v1.6.3.3\ngenerates the following output:\n  ...\n  100644 blob e57630e     walker.c\n  100644 blob 8a149e1     walker.h\n  100644 blob 7eb3218     wrapper.c\n  100644 blob 4c29255     write_or_die.c\n  100644 blob 819c797     ws.c\n  100644 blob 1b6df45     wt-status.c\n  100644 blob 78add09     wt-status.h\n  100644 blob b9b0db8     xdiff-interface.c\n  100644 blob 7352b9a     xdiff-interface.h\n  040000 tree ef5d413     xdiff\n\nthen\n  $ git blame --abbrev v1.6.3.3 -- .\nwould generate\n\n  100644 blob e57630e ba19a80 Junio C Hamano      Feb 10 17:42   walker.c\n  100644 blob 8a149e1 c13b263 Daniel Barkalow     Apr 26  2008   walker.h\n  100644 blob 7eb3218 fc71db3 Alex Riesen         Apr 29 23:21   wrapper.c\n  100644 blob 4c29255 559e840 Junio C Hamano      Jul 20  2008   write_or_die.c\n  100644 blob 819c797 a437900 Junio C Hamano      Jun 21 02:35   ws.c\n  100644 blob 1b6df45 2af202b Linus Torvalds      Jun 18 10:28   wt-status.c\n  100644 blob 78add09 6c2ce04 Marius Storm-Olsen  Jun  5  2008   wt-status.h\n  100644 blob b9b0db8 eb3a9dd Benjamin Kramer     Mar  7 21:02   xdiff-interface.c\n  100644 blob 7352b9a 86295bb Rene Scharfe        Oct 25  2008   xdiff-interface.h\n  040000 tree ef5d413 5719db9 Charles Bailey      May 25 01:21   xdiff/\n\nor something like that.  Date doesn't have to be in this strange format\nused by 'ls'.  Also instead of name we can use username part of email,\nor just email; OTOH git-blame uses above format for author.\n\nThe porcelain / incremental output format for \"git blame <directory>\"\nwouldn't need to be changed much from \"git blame <file>\"; line numbers\ndo not matter though, as what is important is SHA-1 of entry (blob, tree\nor commit).\n  $ git blame --porcelain v1.6.3.3 -- .\nThe blame output for last two lines could look like the following:\n  86295bb6bac1482d29650d1f77f19d8e7a7cc2fe 7352b9a9c204c2b1d4ca9df5ce040fe22d6f521c\n  author Rene Scharfe\n  author-mail <rene.scharfe@lsrfire.ath.cx>\n  author-time 1224941475\n  author-tz +0200\n  committer Junio C Hamano\n  committer-mail <gitster@pobox.com>\n  committer-time 1224961771\n  committer-tz -0700\n  summary add xdi_diff_hunks() for callers that only need hunk lengths\n  filename xdiff-interface.h\n  100644 blob 7352b9a9c204c2b1d4ca9df5ce040fe22d6f521c    xdiff-interface.h\n  5719db91ce5915ee07c50f1afdc94fe34e91529f ef5d413237b3a390007fba56671b00d7c371ae1e\n  author Charles Bailey\n  author-mail <charles@hashpling.org>\n  author-time 1243210874\n  author-tz +0100\n  committer Junio C Hamano\n  committer-mail <gitster@pobox.com>\n  committer-time 1243234594\n  committer-tz -0700\n  summary add xdi_diff_hunks() for callers that only need hunk lengths\n  filename xdiff\n  040000 tree ef5d413237b3a390007fba56671b00d7c371ae1e    xdiff\n\n\nWhat do you think about adding such feature?  \n\nIt either could use infrastructure for better '--follow' implementation,\nor lead to better implementation of '--follow' option (which currently\nas it is now works only for simplest cases).  Probably.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"151406","messageId":"AANLkTinXDGgZEkpgkA_CM2KkOwgjFkR-czdB8b5Es2d4@mail.gmail.com","threadId":"25213","inReplyTo":"201009232302.14925.jnareb@gmail.com","subject":"Re: Listing files in order they are changed","fromName":"Sverre Rabbelier","fromEmail":"srabbelier@gmail.com","sentAt":"2010-09-23T21:20:22Z","receivedAt":"2010-09-23T21:20:22Z","isPatch":false,"sender":{"key":"srabbelier@gmail.com","avatar":"https://avatars.githubusercontent.com/u/3098?v=4"},"body":"Heya,\n\nOn Thu, Sep 23, 2010 at 23:02, Jakub Narebski <jnareb@gmail.com> wrote:\n> Avert your eyes, ugly code ahead ;-)\n\nSadly (luckily?) I don't read perl, so I'll leave that to someone else :).\n\n> What do you think about adding such feature?\n\nIt would be neat for git web etc., and we've seen one use case for it already.\n\n> It either could use infrastructure for better '--follow' implementation,\n> or lead to better implementation of '--follow' option (which currently\n> as it is now works only for simplest cases).  Probably.\n\nOooh, a better '--follow' implementation would be very nice. Definitely!\n\n-- \nCheers,\n\nSverre Rabbelier\n"},{"id":"151452","messageId":"alpine.DEB.2.00.1009240855080.31704@ds9.cixit.se","threadId":"25213","inReplyTo":"20100923203814.GA10951@sigill.intra.peff.net","subject":"Re: Listing files in order they are changed","fromName":"Peter Krefting","fromEmail":"peter@softwolves.pp.se","sentAt":"2010-09-24T07:59:19Z","receivedAt":"2010-09-24T07:59:19Z","isPatch":false,"sender":{"key":"peter@softwolves.pp.se","avatar":"https://avatars.githubusercontent.com/u/990764?v=4"},"body":"Jeff King:\n\n> git log --pretty=format: --name-only |\n>  grep . |\n>  perl -ne 'print unless $seen{$_}++'\n\nNice one!\n\nThat made me come up with this, which shows a timestamp next to each file:\n\n   git log --pretty=format:/%ai --name-only |\n     grep . |\n \tperl -ne 'if (m(^/(.*))) { $date = $1; } else { print \"$date $_\" unless $seen{$_}++; }'\n\n(change %ai into whatever timestamp you want to show)\n\nMutating that into something that calls \"touch\" might be useful for those of \nus who would like an initial check-out to have last-modified times like the \nones CVS would give you.\n\n-- \n\\\\// Peter - http://www.softwolves.pp.se/\n"}]}