git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: Users of git-check-files?

From
Johannes Schindelin <johannes.schindelin@gmx.de>
Date
Aug 3, 2005, 22:09 UTC
Message-ID
<Pine.LNX.4.63.0508032341320.31852@wgmdd8.biozentrum.uni-wuerzburg.de>
In-Reply-To
<Pine.LNX.4.58.0508031055090.3258@g5.osdl.org>
Hi,
On Wed, 3 Aug 2005, Linus Torvalds wrote:
Show 6 quoted lines
> On Wed, 3 Aug 2005, Johannes Schindelin wrote:
>>
>> I try to write a "git annotate" based on the output of git-whatchanged.
>
> You can't. It's fundamentally not doable, because you lose the merge
> information.

That's why I said I need git-rev-tree. In the meantime I discovered, that git-rev-list has a "--parents" option which suits me just fine. Therefore I'd say: kill git-rev-tree.

> So you need to use a combination of git-rev-list _and_ git-whatchanged.

I tried to do without the ugly "script is calling script is calling program" idiom. That is why my attempt at git-annotate (see attached) is so slow.

Show 5 quoted lines
> I have been thinking of adding a "follow this file" mode to git-rev-list,
> which just ignores all children of merges that used the file directly from
> one of the other children. Exactly because then you could have a
> git-rev-list that prunes based on filename, and would potentially be a lot
> more efficient (not follow the "uninteresting" side of a merge).

Let's sit and see if people pick it up at all. If yes, I'd rather rewrite the whole thing in C eventually (it is in perl now and uses hashes quite extensively...).

Show 5 quoted lines
>> P.S.: My only unsolved problem is that git-whatchanged sometimes shows
>> the diffs in the wrong order (clock-skew problem?)
>
> Nope, this is a direct result of two branches modifying the same file in
> parallel. There is no "right" or "wrong" order.

Exactly. But there is a "you probably meant that": the branch in which it was modified last (not counting merges, of course).

Notes:
- You can either annotate by commit (this is the default), or show 
some other informations with the "-f" flag: Try "-f author,commit:8".
- If you don't specify any files, it assumes you mean all files 
(Attention: slow).
- You can start at a commit instead of the current state by specifying
"-c other_commit".
- The list of commits is traversed as output by git-rev-list, i.e. 
chronologically. Each line is marked with the commit whose parent does not 
contain that line.
- I am not at all sure if my handling of merges is sane. The logic is like 
this: If the commit is parent to more than one commit (i.e. a merge), then 
the touched lines are tentatively marked as changed in that commit, but 
are possibly overridden at a later stage.

Ciao, Dscho

#!/usr/bin/perl
use Getopt::Std;
sub usage() {
	print STDERR 'Usage: ${\basename $0} [-s] [-f format] [-c commit] [files...]
	-s		only look at first parent in case of a merge
	-f format	revision format (e.g. "author,commit:8")
	-c commit	start looking at this commit
';
	exit(1);
}

getopts("hsf:c:") or usage(); $opt_h && usage();

$first_parent_only=$opt_s;
sub read_file ($) {
	my $file=$_[0];
	open IN, $file || return 1;
	$orig_line_count=0;
	@lines=();
	@line_handled=();
	@revisions=();
	while(<IN>) {
		$orig_line_count++;
		$lines[$orig_line_count]=$_;
	}
	close IN;
	$orig_line_count>0 || return 2;
	@mapping[1..$orig_line_count]=(1..$orig_line_count);
	return 0;
}
	
sub init_file($$) {
	my $file=$_[0];
	my $head=$_[1];
	if($head eq "") {
		# read current file
		my $ret=read_file("<".$file);
		$ret==0 || return $ret;
		$current_revision="*"x($revision_string_length-1).";";
		handle_diff("git-diff-files -p ".$file."|",1);
	} else {
		if(`git-ls-tree $head $file`=~/^\S+\s+\S+\s+(\S+)/) {
			$sha1=$1;
			my $ret=read_file("git-cat-file blob ".$sha1."|");
			$ret==0 || return $ret;
		} else {
			usage();
		}
	}
	$file_version=0;
	return 0;
}
# mark all lines still unaccounted for
sub mark_all ($) {
	my $mark_lines_as_handled=$_[0];
	foreach $line (@mapping) {
		if($line_handled{$line}==undef) {
			$revisions[$line]=get_revision();
			if($mark_lines_as_handled) {
				$line_handled{$line}=1;
			}
		}
	}
	if($mark_lines_as_handled) {
		$orig_line_count=0;
		@mapping=();
	}
}
# this sub only handles unified diffs
sub handle_diff($$) {
	my $diff=$_[0];
	my $mark_lines_as_handled=$_[1];
	open DIFF, $diff;
	my @new_mapping=();
	my $current_line_nr_minus=1;
	my $current_line_nr_plus=1;
	while(<DIFF>) {
		if(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/) {
			$empty_diff=0;
			$start_minus=$1;
			$count_minus=$2;
			$start_plus=$3;
			$count_plus=$4;
			# if file was created here, were finished
			if($start_minus==0) {
				mark_all($mark_lines_as_handled);
				return;
			}
			# sane check
			$start_minus-$current_line_nr_minus==$start_plus-$current_line_nr_plus
				|| die "invalid diff: $start_minus,$current_line_nr_minus,$start_plus,$current_line_nr_plus";
			if($start_minus-$current_line_nr_minus>0) {
				@new_mapping[$current_line_nr_minus..$start_minus]
					=@mapping[$current_line_nr_plus..$start_plus];
				$current_line_nr_minus=$start_minus;
				$current_line_nr_plus=$start_plus;
			}
			while($count_minus>0 || $count_plus>0) {
				$_=<DIFF>;
				if(/^-/) {
					$new_mapping[$current_line_nr_minus]=undef;
					$current_line_nr_minus++;
					$count_minus--;
				} else {
					if(/^\+/) {
						$orig=$mapping[$current_line_nr_plus];
						if($orig>0 && $line_handled[$orig]==undef) {
							$revisions[$orig]=get_revision();
							if($mark_lines_as_handled) {
								$line_handled[$orig]=1;
								$orig_line_count--;
								if($orig_line_count==0) {
									@mapping=@new_mapping;
									return;
								}
							}
						}
						$current_line_nr_plus++;
						$count_plus--;
					} else {
						$orig_line_nr=$mapping[$current_line_nr_plus];
						$new_mapping[$current_line_nr_minus]=$orig_line_nr;
						if($orig_line_nr>0) {
							# sane check
							if(substr($_,1) ne $lines[$orig_line_nr]) {
								print "--\n";
								print @lines[($orig_line_nr-3)..($orig_line_nr+3)];
								print "--\n";
								die "invalid diff ($orig_line_nr:$current_line_nr_plus): ".substr($_,1)." is not ".$lines[$orig_line_nr];
							}
							substr($_,1) eq $lines[$orig_line_nr] ||
								die "invalid diff ($diff): ".substr($_,1)." is not ".$lines[$orig_line_nr].$lines[1..$#lines];
						}
						$current_line_nr_minus++;
						$current_line_nr_plus++;
						$count_minus--;
						$count_plus--;
					}
				}
			}
		}
	}
	close DIFF;
	$rest_lines=$#mapping-$current_line_nr_plus;
	if($rest_lines>0) {
		@new_mapping[$current_line_nr_minus..($current_line_nr_minus+$rest_lines)]
			=@mapping[$current_line_nr_plus..($current_line_nr_plus+$rest_lines)];
	}
	$file_version++;
	@mapping=@new_mapping;
	return;
}
sub get_revision() {
	if($current_revision eq "") {
		%commit_values=('fileversion','V-'.$file_version);
		$commit_values{'commit'}=$current_commit;
		open COMMIT, "git-cat-file commit $current_commit|";
		while(($_=<COMMIT>) && !/^$/) {
			if(/^parent ([0-9a-f]{40})/) {
				$commit_values{'parents'}.=$1." ";
			} elsif(/^author (.*)/) {
				$commit_values{'author'}=$1;
			} elsif(/^committer (.*)/) {
				$commit_values{'committer'}=$1;
			}
		}
		close COMMIT;
		for($i=0;$i<$#revision_format;$i+=2) {
			my $temp=$commit_values{$revision_format[$i]};
			my $length=length($temp);
			if($length>$revision_format[$i+1]) {
				$temp=substr($temp,0,$revision_format[$i+1]);
			} elsif($length<$revision_format[$i+1]) {
				$temp.=" "x($revision_format[$i+1]-$length);
			}
			$current_revision.=$temp.";";
		}
	}
	return $current_revision;
}
sub show_current {
	for($i=1;$i<=$#lines;$i++) {
		print $revisions[$i].$lines[$i];
	}
}
sub annotate_file($$) {
	my $file=$_[0];
	my $head=$_[1];
	init_file($file,$head);
	my @new_mapping=@mapping;
	%commit_mappings=($commits[0]=>\@new_mapping);
	my $index;
	for($index=0;$index<$#commits;$index++) {
		$current_commit=$commits[$index];
		$current_revision="";
		if($orig_line_count==0 || $children_count{$current_commit}==0) {
			mark_all(1);
			show_current();
			return;
		} else {
			$next_commit=$commits[$index+1];
			if($parent_tree{$next_commit} ne $current_commit) {
				# current_commit has children, so save the line mapping
				my @new_mapping=@mapping; # force copy
				$commit_mappings{$current_commit}=\@new_mapping;
				# get current_commit (which is the parent of next_commit)
				$current_commit=$parent_tree{$next_commit};
				if($commit_mappings{$current_commit}==undef) {
					die "fatal";
				}
				@mapping=@{$commit_mappings{$current_commit}};
				# free memory
				if($children_count{$current_commit}==1) {
					$commit_mappings{$current_commit}=undef; 
				}
			}
			handle_diff("git-diff-tree -p $next_commit $current_commit $file|",$children_count{$current_commit}==1);
			# if there are multiple children, save the line mapping
			if($children_count{$next_commit}>1) {
				my @new_mapping=@mapping; # force copy
				$commit_mappings{$next_commit}=\@new_mapping;
			}
		}
	}
	show_current();
}
sub parse_revision_format($) {
	my $format=$_[0];
	$revision_string_length=0;
	@revision_format=();
	foreach $f (split(",",$format)) {
		if($f=~/^(.*):(.*)/) {
			push @revision_format,($1,$2);
		} else {
			push @revision_format,($f,16);
		}
		$revision_string_length+=$revision_format[$#revision_format]+1;
	}
	if($#revision_format<1) {
		push @revision_format,("commit",40);
		$revision_string_length=$revision_format[$#revision_format]+1;
	}
}
sub get_parent_tree($) {
	my $head=$_[0];
	my $i;
	%parent_tree=();
	%children_count=();
	@commits=();
	$next_commit="";
	open REVS, "git-rev-list --parents ".($head eq ""?"HEAD":$head)."|";
	LOOP: while(<REVS>) {
		if($first_parent_only && $next_commit ne "") {
			while(!/^$next_commit/) {
				if(!($_=<REVS>)) {
					last LOOP;
				}
			}
		}
		my @list=split /[ \n]/;
		push @commits, $list[0];
		for($i=1;$i<=$#list;$i++) {
			$parent_tree{$list[$i]}=$list[0];
		}
		$children_count{$list[0]}=$#list;
		if($first_parent_only) {
			$next_commit=$list[1];
		}
	}
	close REVS;
}

get_parent_tree($opt_c); parse_revision_format($opt_f);

if($#ARGV<0) {
	open FILES, 'git-ls-tree -r '.($opt_c eq ''?'HEAD':$opt_c).'|';
	while(<FILES>) {
		if(/^\S+\s+\S+\s+\S+\s+(.*)$/) {
			push @ARGV, $1;
		}
	}
	close FILES;
}
for($i=0;$i<=$#ARGV;$i++) {
	if($#ARGV>1) {
		print "File: ".$ARGV[$i]."\n";
	}
	annotate_file($ARGV[$i],$opt_c);
}
Previous: Linus TorvaldsNext: Linus Torvalds
Message 6 of 37 in “Users of git-check-files?”
  1. Johannes SchindelinAug 2, 2005
  2. Linus TorvaldsAug 3, 2005
  3. Junio C HamanoAug 3, 2005
  4. Johannes SchindelinAug 3, 2005
  5. Linus TorvaldsAug 3, 2005
  6. Johannes SchindelinAug 3, 2005
  7. Linus TorvaldsAug 3, 2005
  8. Junio C HamanoAug 3, 2005
  9. Linus TorvaldsAug 3, 2005
  10. Junio C HamanoAug 3, 2005
  11. Linus TorvaldsAug 3, 2005
  12. Johannes SchindelinAug 3, 2005
  13. Josef WeidendorferAug 3, 2005
  14. Junio C HamanoAug 3, 2005
  15. Josef WeidendorferAug 3, 2005
  16. Linus TorvaldsAug 3, 2005
  17. (preview) Renaming push.Junio C Hamano, Aug 3, 2005
  18. Linus TorvaldsAug 3, 2005
  19. Junio C HamanoAug 4, 2005
  20. Linus TorvaldsAug 4, 2005
  21. Junio C HamanoAug 4, 2005
  22. Junio C HamanoAug 4, 2005
  23. Linus TorvaldsAug 4, 2005
  24. Junio C HamanoAug 4, 2005
  25. Teach rev-list since..til notation.Junio C Hamano, Aug 4, 2005
  26. Linus TorvaldsAug 3, 2005
  27. Junio C HamanoAug 3, 2005
  28. Linus TorvaldsAug 3, 2005
  29. Junio C HamanoAug 3, 2005
  30. Josef WeidendorferAug 3, 2005
  31. Linus TorvaldsAug 3, 2005
  32. Linus TorvaldsAug 3, 2005
  33. Junio C HamanoAug 3, 2005
  34. Josef WeidendorferAug 3, 2005
  35. Johannes SchindelinAug 3, 2005
  36. Junio C HamanoAug 4, 2005
  37. Johannes SchindelinAug 4, 2005

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.