{"thread":{"id":"24256","subject":"Print last time and committer a file was touched by for a whole repo","startedAt":"2010-07-01T19:05:26Z","lastAt":"2010-07-05T09:53:22Z","messageCount":9,"participants":["Tim Visher","Eric Raible","Jonathan Nieder","Jakub Narebski","Eric Wong"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"144618","messageId":"AANLkTikRElk07ZqK0TOM2WD31t-H5RVngvHNU9KM7e9D@mail.gmail.com","threadId":"24256","inReplyTo":null,"subject":"Print last time and committer a file was touched by for a whole repo","fromName":"Tim Visher","fromEmail":"tim.visher@gmail.com","sentAt":"2010-07-01T19:05:26Z","receivedAt":"2010-07-01T19:05:26Z","isPatch":false,"sender":{"key":"tim.visher@gmail.com","avatar":"https://gravatar.com/avatar/98307ce54bcac1a46b5d845827d5386e2a27895ff08790cdd3877ad527760575?d=mp&s=160"},"body":"Hi everyone,\n\nI need to get a listing of the entire contents of my current repo (as\nin, I don't need deleted files or anything like that, just the current\nsnapshot) with the time the file was committed and who committed it.\n\nThoughts on how to do that?\n\n-- \n\nIn Christ,\n\nTimmy V.\n\nhttp://burningones.com/\nhttp://five.sentenc.es/ - Spend less time on e-mail\n"},{"id":"144619","messageId":"loom.20100701T214421-373@post.gmane.org","threadId":"24256","inReplyTo":"AANLkTikRElk07ZqK0TOM2WD31t-H5RVngvHNU9KM7e9D@mail.gmail.com","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Eric Raible","fromEmail":"raible@gmail.com","sentAt":"2010-07-01T19:45:56Z","receivedAt":"2010-07-01T19:45:56Z","isPatch":false,"sender":{"key":"raible@gmail.com","avatar":null},"body":"Tim Visher <tim.visher <at> gmail.com> writes:\n\n> \n> Hi everyone,\n> \n> I need to get a listing of the entire contents of my current repo (as\n> in, I don't need deleted files or anything like that, just the current\n> snapshot) with the time the file was committed and who committed it.\n> \n> Thoughts on how to do that?\n\nLoop over the output of 'git ls-files' doing\n'git log -1 --pretty=\"%ae %aD\"' on each of them?\n"},{"id":"144620","messageId":"20100701200525.GA3686@burratino","threadId":"24256","inReplyTo":"AANLkTikRElk07ZqK0TOM2WD31t-H5RVngvHNU9KM7e9D@mail.gmail.com","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-07-01T20:05:25Z","receivedAt":"2010-07-01T20:05:25Z","isPatch":false,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Hi Tim,\n\nTim Visher wrote:\n\n> I need to get a listing of the entire contents of my current repo (as\n> in, I don't need deleted files or anything like that, just the current\n> snapshot) with the time the file was committed and who committed it.\n\nYou might be able to adapt Eric’s set-file-times script from [1].\n\nThe set-file-times script was designed to produce consistent\nLast-Modified headers when serving static content from a cluster of\nHTTP servers.  It does not do the right thing for merges (it is\nmissing at least ‘-c’), though it will at least produce consistent\nresults in that case.  See the wiki page for details.\n\nFor reference:\n\n #!/usr/bin/perl -w\n # Use of this script in combination with ‘make’ is asking for trouble.\n\n use strict;\n\n my %ls = ();\n my $commit_time;\n\n $/ = \"\\0\";\n open FH, 'git ls-files -z|' or die $!;\n while (<FH>) {\n\tchomp;\n\t$ls{$_} = $_;\n }\n close FH;\n\n $/ = \"\\n\";\n open FH, \"git log -r --name-only --no-color --pretty=raw -z @ARGV |\"\n or die $!;\n while (<FH>) {\n\tchomp;\n\tif (/^committer .*? (\\d+) (?:[\\-\\+]\\d+)$/) {\n\t\t$commit_time = $1;\n\t} elsif (s/\\0\\0commit [a-f0-9]{40}$// or s/\\0$//) {\n\t\tmy @files = delete @ls{split(/\\0/, $_)};\n\t\t@files = grep { defined $_ } @files;\n\t\tnext unless @files;\n\t\tutime $commit_time, $commit_time, @files;\n\t}\n\tlast unless %ls;\n }\n close FH;\n\nHope that helps,\nJonathan\n\n[1] https://git.wiki.kernel.org/index.php/ExampleScripts#Setting_the_timestamps_of_the_files_to_the_commit_timestamp_of_the_commit_which_last_touched_them\n"},{"id":"144623","messageId":"m3eifnner1.fsf@localhost.localdomain","threadId":"24256","inReplyTo":"AANLkTikRElk07ZqK0TOM2WD31t-H5RVngvHNU9KM7e9D@mail.gmail.com","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-07-01T20:12:21Z","receivedAt":"2010-07-01T20:12:21Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Tim Visher <tim.visher@gmail.com> writes:\n\n> I need to get a listing of the entire contents of my current repo (as\n> in, I don't need deleted files or anything like that, just the current\n> snapshot) with the time the file was committed and who committed it.\n> \n> Thoughts on how to do that?\n\nThere does not exist a single git command that would do what you want.\nYou would need to use 'git log -1 --follow' for each file in current\nsnapshot ('git ls-tree -r HEAD').  IIRC there is some example how to\ndo that in GitFaq or GitTips on git wiki (http://git.wiki.kernel.org).\n\nPerhaps in the future 'git blame <directory>' would provide such\noutput, or its equivalent (tree blame).\n\n\nBy the way, what do you ned this for?  Git versions whole project at\nonce, not individual files.  Is it some legacy from CVS?\n\n-- \nJakub Narebski\nPoland\nShadeHawk on #git\n"},{"id":"144644","messageId":"20100701214035.GA2969@dcvr.yhbt.net","threadId":"24256","inReplyTo":"20100701200525.GA3686@burratino","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Eric Wong","fromEmail":"normalperson@yhbt.net","sentAt":"2010-07-01T21:40:36Z","receivedAt":"2010-07-01T21:40:36Z","isPatch":false,"sender":{"key":"e@80x24.org","avatar":null},"body":"Jonathan Nieder <jrnieder@gmail.com> wrote:\n> Hi Tim,\n> \n> Tim Visher wrote:\n> \n> > I need to get a listing of the entire contents of my current repo (as\n> > in, I don't need deleted files or anything like that, just the current\n> > snapshot) with the time the file was committed and who committed it.\n> \n> You might be able to adapt Eric’s set-file-times script from [1].\n> \n> The set-file-times script was designed to produce consistent\n> Last-Modified headers when serving static content from a cluster of\n> HTTP servers.  It does not do the right thing for merges (it is\n> missing at least ‘-c’), though it will at least produce consistent\n> results in that case.  See the wiki page for details.\n\nYes, \"git log -c\" is more correct, thanks.\n\nI've brought http://yhbt.net/git-set-file-times back up and updated it.\n(I changed servers for yhbt.net around 1.5 years ago and broke\na bunch of links I forgot existed).\n\nSince the rsync developers distribute an outdated (and missing\ndisclaimer) version of this, perhaps we should just include this in the\ncontrib/ section of git.git...\n\n-- \nEric Wong\n"},{"id":"144658","messageId":"20100702033709.GA6818@burratino","threadId":"24256","inReplyTo":"20100701214035.GA2969@dcvr.yhbt.net","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-07-02T03:37:10Z","receivedAt":"2010-07-02T03:37:10Z","isPatch":false,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Eric Wong wrote:\n\n> perhaps we should just include this in the\n> contrib/ section of git.git...\n\nYes, I think so.\n\nThanks for writing it.  I have been thinking of adapting\nthe code for ‘git archive’ for a while now, to make it\neasier to comply with the Debian policy on timestamps[1].\n\n[1] http://www.debian.org/doc/debian-policy/ch-source.html#s-timestamps\n"},{"id":"144677","messageId":"AANLkTil2Riz3_iKEREjsERVsBkSZdZ9wa9ujOahxjs4Y@mail.gmail.com","threadId":"24256","inReplyTo":"m3eifnner1.fsf@localhost.localdomain","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Tim Visher","fromEmail":"tim.visher@gmail.com","sentAt":"2010-07-02T11:55:19Z","receivedAt":"2010-07-02T11:55:19Z","isPatch":false,"sender":{"key":"tim.visher@gmail.com","avatar":"https://gravatar.com/avatar/98307ce54bcac1a46b5d845827d5386e2a27895ff08790cdd3877ad527760575?d=mp&s=160"},"body":"Thanks everyone who responded.  I ended up doing\n\n    find . -path \"./.git*\" -prune -o -print -exec git log -n 1 -- '{}'\n\\; > assets.txt\n\nLittle roundabout but seems effective.\n\nOn Thu, Jul 1, 2010 at 4:12 PM, Jakub Narebski <jnareb@gmail.com> wrote:\n> Tim Visher <tim.visher@gmail.com> writes:\n>\n>> I need to get a listing of the entire contents of my current repo (as\n>> in, I don't need deleted files or anything like that, just the current\n>> snapshot) with the time the file was committed and who committed it.\n>>\n>> Thoughts on how to do that?\n>\n> There does not exist a single git command that would do what you want.\n> You would need to use 'git log -1 --follow' for each file in current\n> snapshot ('git ls-tree -r HEAD').  IIRC there is some example how to\n> do that in GitFaq or GitTips on git wiki (http://git.wiki.kernel.org).\n>\n> Perhaps in the future 'git blame <directory>' would provide such\n> output, or its equivalent (tree blame).\n\nThat'd be cool.\n\n> By the way, what do you ned this for?  Git versions whole project at\n> once, not individual files.  Is it some legacy from CVS?\n\nUmmm...  Little embarrassing but this is apparently a requirement for\nmy company.  Every few years they ask for a 'listing of all software\nassets, when they were last touched, who last touched them, and what\nversion of software they were touched for.'  Generous assumptions is\nthat they're probing us for how effectively we can lay our hands on\nthis information.  Cynics would say that someone somewhere decided one\nday that it would be a good idea to have an __Excel Spreadsheet__\n(yep, that's what it goes into) listing every file that every software\nproject everywhere in the company has, and that now people do it\nbecause it's on a check list.\n\nAnywho... Hooray for `find -exec`.\n\n-- \n\nIn Christ,\n\nTimmy V.\n\nhttp://burningones.com/\nhttp://five.sentenc.es/ - Spend less time on e-mail\n"},{"id":"144737","messageId":"201007031113.33231.jnareb@gmail.com","threadId":"24256","inReplyTo":"AANLkTil2Riz3_iKEREjsERVsBkSZdZ9wa9ujOahxjs4Y@mail.gmail.com","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-07-03T09:13:32Z","receivedAt":"2010-07-03T09:13:32Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 2 July 2010, Tim Visher wrote:\n> Thanks everyone who responded.  I ended up doing\n> \n>     find . -path \"./.git*\" -prune -o -print -exec git log -n 1 -- '{}'\n> \\;> assets.txt\n> \n> Little roundabout but seems effective.\n\nSidenote: you might want to use '--follow' on place of '--', just in rare\ncase you would hit file rename (or copy).\n\nSee also: https://git.wiki.kernel.org/index.php/ExampleScripts#Finding_which_commits_last_touched_the_files\n\n> On Thu, Jul 1, 2010 at 4:12 PM, Jakub Narebski <jnareb@gmail.com> wrote:\n>> Tim Visher <tim.visher@gmail.com> writes:\n>>\n>>> I need to get a listing of the entire contents of my current repo (as\n>>> in, I don't need deleted files or anything like that, just the current\n>>> snapshot) with the time the file was committed and who committed it.\n>>>\n>>> Thoughts on how to do that?\n>>\n>> There does not exist a single git command that would do what you want.\n>> You would need to use 'git log -1 --follow' for each file in current\n>> snapshot ('git ls-tree -r HEAD').  IIRC there is some example how to\n>> do that in GitFaq or GitTips on git wiki (http://git.wiki.kernel.org).\n>>\n>> Perhaps in the future 'git blame <directory>' would provide such\n>> output, or its equivalent (tree blame).\n> \n> That'd be cool.\n\nI am currently working on prototype in Perl, using 'git cat-file --batch'\nand 'git diff-tree --stdin', as I don't know git C code/API enought to\nwrite it in C; it is planned to be converted to C after proof of concept\nworks.\n \n>> By the way, what do you ned this for?  Git versions whole project at\n>> once, not individual files.  Is it some legacy from CVS?\n> \n> Ummm...  Little embarrassing but this is apparently a requirement for\n> my company.  Every few years they ask for a 'listing of all software\n> assets, when they were last touched, who last touched them, and what\n> version of software they were touched for.'  Generous assumptions is\n> that they're probing us for how effectively we can lay our hands on\n> this information.  Cynics would say that someone somewhere decided one\n> day that it would be a good idea to have an __Excel Spreadsheet__\n> (yep, that's what it goes into) listing every file that every software\n> project everywhere in the company has, and that now people do it\n> because it's on a check list.\n> \n> Anywho... Hooray for `find -exec`.\n\nWhy the _files_ granularity, rather than _project_ (repository) \ngranularity?  Unless you have post-CVS / post-Subversion one mega-repo\ncontaining all projects squashed together (yuck!).\n\nIMHVO better solution would be list, for each repository/(sub)project,\nlist date of last commit on master branch (when it was last touched),\nlist date of last signed tag / of tagged release (when it was last \nreleased), and shortlog or blame-based or diffstat based statistics\nof code authorship (replacement of 'who last touched them').  Note\nthat any code metric / software kwalitee metric is subject to abuse\n(numerous examples can be found at TheDailyWTF, and IIRC Joel Spolsky\nand Jeff Atwood both described such dangers on their blogs).\n\n-- \nJakub Narebski\nPoland\n"},{"id":"144807","messageId":"201007051153.23956.jnareb@gmail.com","threadId":"24256","inReplyTo":"201007031113.33231.jnareb@gmail.com","subject":"Re: Print last time and committer a file was touched by for a whole repo","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-07-05T09:53:22Z","receivedAt":"2010-07-05T09:53:22Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Sat, 3 July 2010, Jakub Narebski wrote:\n> On Fri, 2 July 2010, Tim Visher wrote:\n>> On Thu, Jul 1, 2010 at 4:12 PM, Jakub Narebski <jnareb@gmail.com> wrote:\n>>> Tim Visher <tim.visher@gmail.com> writes:\n>>>\n>>>> I need to get a listing of the entire contents of my current repo (as\n>>>> in, I don't need deleted files or anything like that, just the current\n>>>> snapshot) with the time the file was committed and who committed it.\n>>>>\n>>>> Thoughts on how to do that?\n>>>\n>>> There does not exist a single git command that would do what you want.\n>>> You would need to use 'git log -1 --follow' for each file in current\n>>> snapshot ('git ls-tree -r HEAD').  IIRC there is some example how to\n>>> do that in GitFaq or GitTips on git wiki (http://git.wiki.kernel.org).\n>>>\n>>> Perhaps in the future 'git blame <directory>' would provide such\n>>> output, or its equivalent (tree blame).\n>> \n>> That'd be cool.\n> \n> I am currently working on prototype in Perl, using 'git cat-file --batch'\n> and 'git diff-tree --stdin', as I don't know git C code/API enought to\n> write it in C; it is planned to be converted to C after proof of concept\n> works.\n\nAnd it even works[1]... but only for a top directory, because of bug in\n--relative=<path> implementation for --raw / git-diff-tree output, see\nhttp://permalink.gmane.org/gmane.comp.version-control.git/150248\n\n[1] But I have not tested it very extensively.\n[2] It is also missing some features.\n\n-- \nJakub Narebski\nPoland\n\n\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\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\") : ());\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@difftree = map { { parse_difftree_raw_line($_) } } @difftree;\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\";\n#my $tree_path    = \"sub\";\nmy $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"}]}