Subject: [BUG/RFC] Raw diff output format (git-diff-tree) and --relative[=<path>] option

From: Jakub Narebski <[email-masked]>
Date: Thu, 8 Jul 2010 13:00:17 +0200

I wanted to get non-recursive raw diff (difftree), but for a given
subdirectory and not starting from root.

In February 2008 Junio C Hamano added support for --relative and 
--relative=<path> options to git-diff:
* cd676a5 (diff --relative: output paths as relative
           to the current subdirectory, 2008-02-12)
* c0cb4a0 (diff --relative: help working in a bare repository,
           2008-02-13)

>>From the commit message for cd676a5 (the c0cb4a0 just allows the 
--relative option to say which subdirectory to pretend to be in,
i.e. adds the --relative=<path> version) it looks like this option
was intended for patch (-p) output format.

There was added support also for raw output format, so both 
'git diff-tree' and 'git diff --raw' works with --relative option,
but support for this is buggy, and in my opinion wrong way around.

The '--relative[=<path>]' option works currently like this.  First,
if git command is invoked in subdirectory the diffopts structure
gets set prefix and prefix_length (in init_revisions).  If 
--relative[=<path>] option is passed, git sets RELATIVE_NAME flag,
and if there is argument, prefix is set to it.  Later git removes
prefix (sets it to NULL) if RELATIVE_NAME option is not set.

What's important in this step is that prefix is set without any 
normalization from --relative=<path> argument, while (from what
I understand) if it is set from current directory i.e. with --relative 
option without argument, it is set with trailing slash.  So using
--relative=sub instead of --relative=sub/ might be thought as user
error... but I think here it is lack of robustness in the API.

Then comes the filtering part.  In functions such as diff_change
or stuff_change, if path(s) does not begin with prefix, they are
simply skipped.  This solution limits --relative[=<path>] to work
only with *recursive* (full tree) output, such as patch output format,
or "git diff", or "git diff-tree -r" (and "git diff-tree -t", which
implies "-r").

Last there is filename munging, done using strip_prefix function.
This is done using prefix_length only, and that is the cause of
the bug:
  $ git diff-tree --abbrev -r --raw HEAD --relative=sub
  a3a8425fe5496c61921010cb1e7b455a1f52bb86
  :100644 100644 d90bda0... cefcae0... M	/quux

if one uses '--relative=sub' instead of '--relative=sub/'.

What I'd like to see for the raw output format is to work with 
--relative[=<path>] to work as if <path> was top directory of 
repository.  For example for diff between two trees

  $ git diff-tree A B --relative=sub/

would be equivalent to running

  $ git diff-tree A:sub/ B:sub/

*This* could be done, I think, by modifying diff_tree_sha1 to do a diff 
betweem A:sub/ and B:sub/ (taking 'sub/' from prefix) and unsetting 
prefix (setting prefix to NULL and prefix_length to 0).  But that would 
work only in the case that can be reducted to diff between two tree 
objects.  This wouldn't work for diff in raw output format between tree 
and working area, tree and index, or index and working area.

Is the idea of automagically translating <sha1> into <sha1>:<prefix>
to support --relative / relative=<prefix> well in raw diff output format
a good idea?  Or should I search for another solution.

I also do not know code enough (and it is not simple) to guess how
one would go with the same result for diff between trees, index, and
working area files.

BTW. the approach proposed here has the advantage that for B:<sub>,
if <sub> does not exist in B, we can try to do what 'subtree' merge 
strategy does (and what wholesame directory rename detection did),
namely try to find given directory under different path (like for 
example subtree-merged git-gui and gitk).

-- 
Jakub Narebski
Poland