xaizek / zograscope (License: AGPLv3 only) (since 2018-12-07)
Mainly a syntax-aware diff that also provides a number of additional tools.
<root> / tools / diff / diff.cpp (485e3a772927866264f6914c70c38b9af2ccd88c) (7,557B) (mode 100644) [raw]
// Copyright (C) 2017 xaizek <xaizek@posteo.net>
//
// This file is part of zograscope.
//
// zograscope is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public License as
// published by the Free Software Foundation.
//
// zograscope is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with zograscope.  If not, see <http://www.gnu.org/licenses/>.

#include <sys/wait.h>
#include <unistd.h>

#include <boost/optional.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/variables_map.hpp>

#include <algorithm>
#include <functional>
#include <future>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

#include "pmr/monolithic.hpp"

#include "tooling/common.hpp"
#include "utils/optional.hpp"
#include "Printer.hpp"
#include "compare.hpp"
#include "decoration.hpp"
#include "tree.hpp"

// Tool-specific type for holding arguments.
struct Args : CommonArgs
{
    bool gitDiff;       // Invoked by git and file was changed.
    bool gitRename;     // File was renamed and possibly changed too.
    bool gitRenameOnly; // File was renamed without changing it.
};

static Args parseLocalArgs(const Environment &env);
static int run(Environment &env, const Args &args);
static int gitFallback(const Args &args);

int
main(int argc, char *argv[])
{
    Args args = { };
    int result;

    try {
        Environment env;
        env.setup({ argv + 1, argv + argc });

        args = parseLocalArgs(env);
        if (args.help) {
            std::cout << "Usage: zs-diff [options...] old-file new-file\n"
                      << "   or: zs-diff [options...] <7 or 9 args from git>\n"
                      << "\n"
                      << "Options:\n";
            env.printOptions();
            return EXIT_SUCCESS;
        }
        if (args.pos.size() != 2U && !args.gitDiff && !args.gitRename) {
            env.teardown(true);
            std::cerr << "Wrong positional arguments\n"
                      << "Expected 2 (cli) or 7 or 9 (git)\n";
            return EXIT_FAILURE;
        }

        result = run(env, args);

        env.teardown();
    } catch (const std::exception &e) {
        std::cerr << "ERROR: " << e.what() << '\n';
        result = EXIT_FAILURE;
    }

    if (result != EXIT_SUCCESS && args.gitDiff) {
        return gitFallback(args);
    }

    return result;
}

static Args
parseLocalArgs(const Environment &env)
{
    Args args;
    static_cast<CommonArgs &>(args) = env.getCommonArgs();

    args.gitDiff = args.pos.size() == 7U
                || (args.pos.size() == 9U && args.pos[2] != args.pos[5]);
    args.gitRename = (args.pos.size() == 9U);
    args.gitRenameOnly = (args.gitRename && args.pos[2] == args.pos[5]);

    return args;
}

static int
run(Environment &env, const Args &args)
{
    if (args.gitRenameOnly) {
        std::cout << (decor::bold << "{ renamed without changes }\n")
                  << (decor::bold << "  old name: " << args.pos[0]) << '\n'
                  << (decor::bold << "  new name: " << args.pos[7]) << '\n';
        return EXIT_SUCCESS;
    }

    cpp17::pmr::monolithic mrA, mrB;
    Tree treeA(&mrA), treeB(&mrB);

    using overload = optional_t<Tree> (*)(Environment &,
                                          TimeReport &,
                                          const Attrs &,
                                          const std::string &,
                                          cpp17::pmr::memory_resource *);
    overload func = &buildTreeFromFile;

    const std::string oldFile = (args.gitDiff ? args.pos[1] : args.pos[0]);
    const std::string newFile = (args.gitDiff ? args.pos[4] : args.pos[1]);

    const int newNameIdx = (args.gitRename ? 7 : 0);

    // Using new file for attributes under assumption that it better matches
    // user's expectations (e.g., new location reflects file's properties
    // better).
    Attrs attrs = env.getConfig().lookupAttrs(args.pos[newNameIdx]);

    TimeReport &tr = env.getTimeKeeper();
    TimeReport nestedTr(tr);
    std::future<optional_t<Tree>> newTreeFuture = std::async(std::launch::async,
                                                             func,
                                                             std::ref(env),
                                                             std::ref(nestedTr),
                                                             attrs,
                                                             newFile,
                                                             &mrB);

    if (optional_t<Tree> &&tree = func(env, tr, attrs, oldFile, &mrA)) {
        treeA = *tree;
    } else {
        // Wait the other thread to finish to avoid data races.
        newTreeFuture.wait();

        std::cerr << "Failed to parse: " << oldFile << '\n';
        return EXIT_FAILURE;
    }

    if (optional_t<Tree> &&tree = newTreeFuture.get()) {
        treeB = *tree;
    } else {
        std::cerr << "Failed to parse: " << newFile << '\n';
        return EXIT_FAILURE;
    }

    if (args.dryRun) {
        dumpTrees(args, treeA, treeB);
        return EXIT_SUCCESS;
    }

    compare(treeA, treeB, tr, !args.fine, /*skipRefine=*/false);

    dumpTrees(args, treeA, treeB);

    Printer printer(*treeA.getRoot(), *treeB.getRoot(), *treeA.getLanguage(),
                    std::cout);
    if (args.gitDiff) {
        printer.addHeader({ args.pos[3], args.pos[6] });
        printer.addHeader({ "a/" + args.pos[0], "b/" + args.pos[newNameIdx] });
    } else {
        printer.addHeader({ oldFile, newFile });
    }
    printer.print(tr);

    return EXIT_SUCCESS;
}

static int
gitFallback(const Args &args)
{
    auto isValid = [](const std::string &hash) {
        // At least older versions of git passed 40 zeroes.
        return (hash != "." && hash != std::string(40U, '0'));
    };

    std::cout << "Parsing has failed, falling back to `git diff`\n";

    // Print only a header by passing in an empty tree.
    Node n;
    std::unique_ptr<Language> l = Language::create(args.pos[0]);
    Printer printer(n, n, *l, std::cout);
    printer.addHeader({ args.pos[3], args.pos[6] });
    printer.addHeader({ "a/" + args.pos[0], "b/" + args.pos[0] });
    TimeReport tr;
    printer.print(tr);

    std::cout.flush();

    bool isAddition = !isValid(args.pos[2]);
    bool isRemoval = !isValid(args.pos[5]);

    if (!isAddition && !isRemoval) {
        execlp("git", "git", "diff", "--no-ext-diff", args.pos[2].c_str(),
               args.pos[5].c_str(), "--", static_cast<char *>(nullptr));
        return 127;
    }

    // The form of git invocation used below implies --exit-code, which can't be
    // disabled.  Fork, exec, wait and ignore exit code unless something is
    // really off.

    pid_t pid = fork();
    if (pid == -1) {
        exit(127);
    }
    if (pid == 0) {
        execlp("git", "git", "diff", "--no-ext-diff", "--",
                args.pos[1].c_str(), args.pos[4].c_str(),
                static_cast<char *>(nullptr));
        exit(127);
    }

    int wstatus;
    if (waitpid(pid, &wstatus, 0) == -1 || !WIFEXITED(wstatus) ||
        WEXITSTATUS(wstatus) == 127) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/zograscope

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/zograscope

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master