What git add -p <file>
does is, very roughly, this:
tmpfile=$(mktemp)
tf2=$(mktemp)
tf3=$(mktemp)
git diff <file> > $tmpfile
while [ -s $tmpfile ]; do
extract first diff hunk from $tmpfile to $tf2 and rest to $tf3
show you $tf2, ask if you want to include this hunk
(with options to edit the hunk, etc); repeat until ready
if you say to *add* the hunk, run git apply --cached $tf2
cat < $tf3 > $tf2
done
rm -f $tmpfile $tf2 $tf3
That is, git add -p
uses git apply --cached
(a specialized sub-variant of git apply --index
that ignores the working tree copy of the file). The key takeaway you need, from the above, is this: There are three versions of the file!
- The first one (completely ignored here) is frozen for all time and is in the
HEAD
commit.
- The second one is in Git's index aka staging area. That's used by
git diff
above as the "old version".
- The third one is in your working tree. That's used by
git diff
above as the "new version".
The patches that Git lets you take or skip are simply the result of comparing the "old" (index) and "new" (working tree) version. If you take some patch, Git updates the in-index copy by applying the patch.
Hence, if there are some set of lines in the working tree version (say, lines 100 through 110 inclusive) that you'd like to use to replace some other set of lines (say, lines 90 through 92 inclusive) in the index version, the way to construct that is:
- extract the index version;
- scrape out lines 1-89 from the index version; concatenate lines 100-110 from the working tree version; concatenate lines 93-end from the index version, all into a temporary file;
- replace the index copy with the temporary file.
To read the index version, use git show
or git cat-file -p
with the name of the index version of the file. If the file's name is path/to/file
, the index version's name is :path/to/file
(short for :0:path/to/file
: we want the copy in slot zero; there must not be a copy in slots 1, 2, or 3 so that there is a copy in slot 0; you can simply attempt to read it from slot zero, and if that fails, assume the file either isn't in the index, or is conflicted).
Reading the working tree file (some select subset of lines) is left as an exercise, as is the concatenation part, and any error checking you wish to include.
Assuming the final resulting file is in a temporary file named $tf
(as a shell variable), to update the index copy, you must first make sure an appropriate blob hash ID exists:
hash=$(git hash-object -w -t blob --path="$path" -- "$tf")
for instance (this assumes you want to run the usual .gitattribute
filters, if any, and know that the path is $path
). Then, if that goes well, use that hash ID with git update-index
:
git update-index --cacheinfo "$mode,$hash,$path"
where $mode
is either 100644
or 100755
as appropriate for the file. If you don't want to change the mode, you can read the previous mode with git ls-files --cached
or similar. Otherwise, provided core.fileMode
is true
, read the mode from the working tree copy of the file, to match the behavior of git add
: convert "has any executable bit set" to 100755
and "has no executable bit set" to 100644
. When core.fileMode
is false
—use git config --get --type bool core.filemode
to read it—git add
uses the existing mode for this add-patch case.)