How to pretty format an arbitrary diff of diffs?

533 Views Asked by At

When rebasing a git branch, especially after conflicts resolution, I like comparing the two patches, i.e. the diff between the main branch and my branch before rebasing and the diff between the main branch and my branch after rebasing. The reason why I do this is to make sure I resolved the conflicts properly and did not introduce bugs as a result of that.

Fortunately, git 2.19 introduced the wonderful git range-diff. And most of the time, it does exactly what I expect. It takes all the commits, before and after the rebase, match them one by one and show what's different. Reallly neat.

Sometimes, however, the conflicts resolution is such a pain and the number of commits to rebase is so high that I simply decide to squash them and resolve all the conflicts at once. And now I have a problem. Before updating the branch, I had N commits, but after that, I only have a single commit left which the result of squashing the N commits on top of the main branch. So range-diff cannot help me anymore. Even if the two patches are very similar, the number of commits differ so they cannot be matched one by one.

An alternative could be to dump the two diffs into two files and then compare those. While that would definitely work, the result would not be very readable. Here's a quick comparison. First, a diff of diffs:

enter image description here

Then, a range-diff:

enter image description here

I think we can easily agree that the latter is far easier to read. Another approach could be to squash the commits before conflicts resolution as well. This way I only have a single commit on both sides and range-diff can be used again. That would work too, but I'm a lazy guy. I don't want to run extra git commands and produce garbage commits for one-time use. Also, I could get it wrong. And that would be a shame since I do this to double-check that I did things properly in the first place.

So my question is the following: Is there another alternative? I want to be able to produce a diff of two arbitrary patches that don't necessarily have the same number of commits. But I also like the way range-diff formats the diff into something very easy to read. Is there another way to make it work? Is there any third-party tools which achieve similar results?

1

There are 1 best solutions below

0
Robin Dos Anjos On BEST ANSWER

I know it's been a while. I'm now using a home-made solution but never took the time to share it here. Sorry for procrastinating!

I basically wrote a NodeJS script which performs the diffing of diffs manually by running multiple git commands successively. Then the output is colorized in such a way that, in combination with less -R, it will be shown exactly like a git range-diff.

The script has a few side effects as it will write two files on disk to run the diff against them and then delete them. It should be fine for most users but I think it's still worth mentioning.

You can then use it in the following way:

node range-diff.js <rev1> <number-of-commits-1> <rev2> <number-of-commits-2>

This will first compute the following two diffs and write them on disk:

git diff <rev1>~<number-of-commits-1> <rev1> > diffA
git diff <rev2>~<number-of-commits-2> <rev2> > diffB

Then it computes the diff of diffs and colorizes it:

git diff --no-index diffA diffB

This is incredibly handy, especially if I've squashed and rebased a topic branch. It lets me compare the changes across N commits against the changes made in a single commit post-rebase:

node range-diff.js topic 10 topic-rebased 1

In the example above, I squashed ten commits into one and then rebased it. And I'm still able to compare the two histories.

You can pipe the output into less to enjoy the nice colors:

node range-diff.js topic 10 topic-rebased 1 | less -R

I think this should be a built-in feature of git. I use it very very often.

Here's the code:

const { spawn } = require('child_process');
const { join } = require('path');

const from = process.argv[2];
const m = process.argv[3];

const to = process.argv[4];
const n = process.argv[5];

const diffA = '"' + join(__dirname, 'diffA') + '"';
const diffB = '"' + join(__dirname, 'diffB') + '"';

function diff(commit, numberOfAncestors, patchFile) {
    return `git diff ${commit}~${numberOfAncestors} ${commit} > ${patchFile}`;
}

function rangeDiff(patchFile1, patchFile2) {
    return `git --no-pager diff --no-index ${patchFile1} ${patchFile2} || rm ${patchFile1} ${patchFile2}`;
}

function getPrefixColor(firstChar) {
    const PREFIX_COLORS = {
        '-': '\033[41m\033[30m',
        '+': '\033[42m\033[30m',
    };

    return PREFIX_COLORS[firstChar] || '';
}

function getTextTransparency(firstChar) {
    const TEXT_TRANSPARENCY = {
        '-': '\033[2m',
        '+': '\033[1m',
    };

    return TEXT_TRANSPARENCY[firstChar] || '';
}

function getTextColor(firstChar, secondChar) {
    let color = getTextTransparency(firstChar);

    const TEXT_COLORS = {
        '-': '\033[31m',
        '+': '\033[32m',
    };

    color += TEXT_COLORS[secondChar] || '';
    return color;
}

function colorLine(line) {
    const RESET_COLORS = '\033[0m';
    const firstChar = line.charAt(0);
    const secondChar = line.charAt(1);

    return (
        getPrefixColor(firstChar) + firstChar + RESET_COLORS +
        getTextColor(firstChar, secondChar) + line.slice(1) + RESET_COLORS
    );
}

function run(command) {
    return new Promise((resolve, reject) => {
        let output = '';
        let error = '';

        const process = spawn('/bin/sh');
        process.stdin.write(command);
        process.stdin.end();

        process.stdout.on('data', data => output += data);
        process.stderr.on('data', data => error += data);

        process.on('close', () => error ? reject(error) : resolve(output));
    });
}

run(`${diff(from, m, diffA)} && ${diff(to, n, diffB)} && ${rangeDiff(diffA, diffB)}`)
    .then(out =>
        console.log(out.toString().split('\n').map(colorLine).join('\n'))
    );