user3586664
user3586664

Reputation: 427

prevent file with merge conflicts from getting committed in git

Is there any way of preventing files with merge conflict from getting committed in git? No one is going to commit files with conflict intentionally. But is there a way to prevent files from getting committed in git?

Does git have any settings or configurable values anywhere, where it can prevent files by looking for <<<<<<<, ======= or >>>>>>> symbols?

Upvotes: 28

Views: 6293

Answers (5)

Mark
Mark

Reputation: 2003

A client side solution is using git pre-commit hooks which allow you to execute specific commands when you commit (a merge is a type of commit) and if they fail, then you cannot merge until it's resolved.

pre-commit is a simple way and standardised to manage git pre-commit hooks.

In the root of your repository create a file called .pre-commit-config.yaml With the following

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.0.1
    hooks:
    -   id: check-merge-conflict
Install pre-commit

(preferred way is to) use pipx or do this outside a virtual env since it's a CLI application

python3 -m pip install pre-commit 
Install the hooks
pre-commit install # you only do this once per "git clone"

Next time you merge if there are conflict markers detected it will warn you and prevent you from continuing

$ git merge --continue
[INFO] Checking merge-conflict files only.
Check for merge conflicts................................................Failed
- hook id: check-merge-conflict
- exit code: 1

Merge conflict string "<<<<<<< " found in README.rst:1
Merge conflict string "=======
" found in README.rst:3
Merge conflict string ">>>>>>> " found in README.rst:5

Upvotes: 1

pattivacek
pattivacek

Reputation: 5833

A straightforward approach using a pre-commit hook, adapted from here but improved to be a bit more careful and thorough:

#!/bin/sh

changed=$(git diff --cached --name-only)

if [[ -z "$changed" ]]; then
    exit 0
fi

echo $changed | xargs egrep '^[><=]{7}( |$)' -H -I --line-number

# If the egrep command has any hits - echo a warning and exit with non-zero status.
if [ $? == 0 ]; then
    echo "WARNING: You have merge markers in the above files. Fix them before committing."
    echo "         If these markers are intentional, you can force the commit with the --no-verify argument."
    exit 1
fi

Don't forget to make the hook executable (chmod u+x pre-commit)!

I've since put this on github: https://github.com/patrickvacek/git-reject-binaries-and-large-files/blob/master/pre-commit

Upvotes: 3

Richard Long
Richard Long

Reputation: 281

I added a unit test to go through all files in the solution directory for the conflict marker string

[TestClass]
public class SolutionValidationTests
{
    [TestMethod]
    public void CheckForMergeConflicts()
    {
        var solutionValidationScripts = new SolutionValidationScripts();
        var mergeConflictCheckOkay = solutionValidationScripts.CheckForGitMergeConflict();
        Assert.IsTrue(mergeConflictCheckOkay);
    }
}

SolutionValidationScripts defined separately below:

public class SolutionValidationScripts
{
    public bool CheckForGitMergeConflict()
    {
        var failCount = 0;
        System.Diagnostics.Debug.WriteLine($"{DateTime.Now.ToString(@"dd-MMM-yyyy HH:mm:ss")}: Starting");

        var currentDirectory = System.IO.Directory.GetCurrentDirectory();
        var sourceFolder = "";

        // Change to suit the build location of your solution
        sourceFolder = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(System.IO.Directory.GetCurrentDirectory())));
        // break up the string so this file doesn't get flagged in the test
        string searchWord = "<<<<<<< " + "HEAD";

        List<string> allFiles = new List<string>();
        AddFileNamesToList(sourceFolder, allFiles);
        for (int i = 0; i < allFiles.Count; i++)
        {
            // 35 sec
            var fileName = allFiles[i];
            string contents = File.ReadAllText(fileName);
            if (contents.Contains(searchWord))
            {
                // For faster result.. no need to continue once a problem is found
                // throwing an exception here actually works better than an early return to help resolve the issue
                throw new Exception(fileName);
            }
        }
        return (failCount == 0);
    }

    private void AddFileNamesToList(string sourceDir, List<string> allFiles)
    {
        string[] fileEntries = Directory.GetFiles(sourceDir);
        foreach (string fileName in fileEntries)
        {
            allFiles.Add(fileName);
        }

        //Recursion    
        string[] subdirectoryEntries = Directory.GetDirectories(sourceDir);
        foreach (string item in subdirectoryEntries)
        {
            // Avoid "reparse points"
            if ((File.GetAttributes(item) & FileAttributes.ReparsePoint) != FileAttributes.ReparsePoint)
            {
                AddFileNamesToList(item, allFiles);
            }
        }
    }
}

Upvotes: 0

sleske
sleske

Reputation: 83645

VonC's answer already explains the different kinds of hooks where you might want to check for merge commits.

If you just want a simple solution to avoid committing conflicts, that is already included with git in the default pre-commit hook sample. Just enable the hook by renaming .git/hooks/pre-commit.sample to .git/hooks/pre-commit. If you then try to commit a conflict:

$ git commit -am "Fix crash in frobnicate"
src/foo.c:239: leftover conflict marker

Note:

The script uses git diff --check internally, which also checks for various white space problems - so you might get whitespace errors as well. You can also run git diff --check before committing to find problems. See the manpage of git diff for details and config options.

This is valid for git V2.0; no idea when it was introduced.

Upvotes: 12

VonC
VonC

Reputation: 1329472

You can use a pre-commit hook, but be aware that a git commit --no-verify would effectively ignore that.

I generally put a pre-receive hook in order to control in a (more) central point what is being pushed.

But a pre-commmit allows for a more timely detection (earlier in the development cycle).

Here is another example (in addition of jthill's comment), in perl.
It uses git diff-index -p -M --cached HEAD, that is git diff-index instead of git diff.
I have left a few other controls done by this hook, just to showcase the kind of checks you can do in such a script.

#!/usr/bin/perl

use Modern::Perl;
use File::Basename;

my $nb_errors = 0;
my $filepath;
for my $l ( split '\n', `git diff-index -p -M --cached HEAD` ) {
    if ( $l =~ /^diff --git a\/([^ ]*) .*$/ ) {
        $filepath = $1;
    }
    given ( $l ) {
        # if there is a file called *.log, stop
        when ( /\.log/ ) {
            say "$filepath contains console.log ($l)";
            $nb_errors++;
        }
        # check if there is a warn, that could be unconditionnal, but just warn, don't stop
        when ( /^[^(\#|\-)]+warn/ ) {
            # stay silent if we're in a template, a "warn" here is fair, it's usually a message or a css class
            unless ($filepath =~ /\.tt$/) {
            say "$filepath contains warn ($l)";
            }
        }
        # check if there is a ` introduced, that is a mysqlism in databases. Can be valid so don't stop, just warn
        when (/^\+.*`.*/) {
            say "$filepath contains a ` ($l)";
        }
        # check if there is a merge conflict marker and just warn (we probably could stop if there is one)
        when ( m/^<<<<<</ or m/^>>>>>>/ or m/^======/ ) {
            say "$filepath contains $& ($l)";
        }
    }
}

if ( $nb_errors ) {
    say "\nAre you sure you want to commit ?";
    say "You can commit with the --no-verify argument";
    exit 1;
}
exit 0;

Upvotes: 7

Related Questions