© Johan Abildskov 2020
J. AbildskovPractical Githttps://doi.org/10.1007/978-1-4842-6270-2_7

7. Customizing Git

Johan Abildskov1 
(1)
Tilst, Denmark
 

Git is an engineer’s tool, built by engineers. This means that while Git works a certain way out of the box, the real power is unleashed when we start customizing Git to match our way of working. With Git, we can do a lot in terms of simple configurations, creating shortcuts for the tasks that we often use, or have repository-specific configurations to help us manage the different contexts in which we work.

But Git does not stop there. Using hooks, we can inject scripts into the normal workflow of Git operations, to better support our workflows, and using Git attributes and filters, we can change the most basic operation of Git, how files are moved between the repository and the workspace. In this chapter, we will go through everything from the most basic configuration and alias to customization that changes some of the fundamental behaviors of Git.

Configuring Git

Git supports three levels of configurations: system, user, and repository local. In most scenarios, we only use the user configurations. System configurations are rarely used and could be used to some effect in multiuser environments to enforce some sane defaults. Repository local configurations are something that we as ordinary Git users could use to a much greater extent. Git applies configurations in the following order: system, user, and local. Each grouping overwrites any duplicate entries from the previous. This is illustrated in Figure 7-1.
../images/495602_1_En_7_Chapter/495602_1_En_7_Fig1_HTML.jpg
Figure 7-1

In the global configuration, user.name is set to briana, while in Repo A, there is a .gitconfig file specifying user.name to be phillip. Thus, in global scope and Repo B, user.name will resolve to briana, while it will resolve to phillip in Repo A. In the system configuration, the default editor is set to emacs

Applying configurations in Git is done through the interface git config. If we add --list to the command, we will read rather than set values. We use a key/value pair to set a configuration. Using the flags --global and --system, we can set user or system configurations, rather than the default repository local configurations. To set the pull strategy to always rebase for the current user, we would run the command git config --global pull.rebase true. If we rather wanted to set it for either the system, we would use --system, or to put it in the repository configuration, drop the --global flag. There are many configurations in Git, and we will not cover them here. Specific configurations can be found in the Git documentation. We will however cover Git configurations in the sense that they enable the next three sections.

GIT CONFIGURATION EXERCISE

In this exercise, we go through tweaking Git configurations. The repository for this exercise can be found in the exercises delivered with the book in the folder chapter7/.

In this exercise, we have two repositories config-ACME and config-AJAX that we are going to use to investigate how configurations overlap. First, we run the script setting up the exercise and then we can move on. Note, you might have issues running this in a non-bash prompt.
$ ./config.sh
$ cd config
$ ls
config-ACME/  config-AJAX/
$ git config user.email
Here, we note that even though we are not in a Git repository, we have access to the configuration. Local configuration does not make any sense in this case. It is also unlikely that you will get the same email returned as I do.
$ cd config-ACME
$ git config user.email
janedoe@acme
Entering the ACME repository, we can see that the user’s email is now different. We access the local and global scope to verify the source of this configuration.
$ git config --local user.email
janedoe@acme
$ git config --global user.email
We can also obtain the same information with the flag --show-origin.
$ git config --show-origin user.email
file:.git/config        janedoe@acme
Now, we go to the other repository to see what values we get.
$ cd ..
$ cd config-AJAX/
$ git config user.email
$ git config --local user.email
$ git config --show-origin user.email
file:C:/Users/Rando/.gitconfig   [email protected]

In this repository, we notice that the local user.email is not set, so we access the user defined instead. We verify this using --show-origin.

The user.email configuration is a part of Git out of the box, but we can also add arbitrary configurations for our own purposes. In these repositories, we are working with a custom configuration that we have called practical-git. We can have multiple entries in our sections, each with a name, but we are working with the company key.
cd ../config-ACME
$ git config practical-git.company
ACME
In the ACME repository company contains the value ACME , let’s check in AJAX.
$ cd ../config-AJAX/
$ git config  practical-git.company
UNKNOWN
Here, we receive the value UNKNOWN, so let’s set the configuration to AJAX.
$ git config practical-git.company AJAX
$ git config practical-git.company
AJAX
We can still access the global scope.
$ git config --global practical-git.company
UNKNOWN
Now that we have contaminated your global configuration space with this section, we will delete this section to remove this from your configuration file.
$ git config --remove-section --global practical-git
$ git config --get --global practical-git.company

This concludes the exercise. We have gone through the user and local scope and how you can have different configurations in different repositories. This can be particularly useful if you use the same computer to personal, open source, and company projects.

Aliases

In Git, we can use aliases to construct shortcuts or extend Git’s functionality. We can either use commands that are native to Git or invoke external commands. A frequent target for aliases is making your logs aligned perfectly with your particular tastes. My go-to log command is git log --oneline --decorate --graph --all which is a long string to type, leaving ample room for typos and other errors. Commonly, I am unable to spell --oneline correctly. In this scenario, I could create an alias for that command. There is no direct alias command, but we can use git config to set aliases. Note that this also means that we can have differently scoped aliases.

GIT ALIAS EXERCISE

In this exercise, we are going to set up some aliases for common tasks in our repository. The repository for this exercise can be found in chapter7/aliases.

I often use a rather long variation of log to investigate repositories.
$ git log --decorate --oneline --graph --all
$ git log --decorate --oneline --graph --all
* b5566ae (myBranch) 7
* 506bb29 6
* f662f41 5
* bd90c39 (HEAD -> master) 5
* 55936c5 4
* 6519696 3
| * e645e36 (newBranch) 9
| * d5ed404 8
|/
* 0425411 (tag: originalVersion) 2
* 11fbef7 1
Of course, this is tedious and often leads to typos and my not remembering what parts I actually want to add. So let us set up an alias for this command. We set up all the aliases in the local repository so we do not leak into our global scope.
$ git config --local alias.l "log --oneline --decorate --graph --all"
This allows us to use git l as a shortcut to the longer variation.
$ git l
$ git log --decorate --oneline --graph --all
* b5566ae (myBranch) 7
* 506bb29 6
* f662f41 5
* bd90c39 (HEAD -> master) 5
* 55936c5 4
* 6519696 3
| * e645e36 (newBranch) 9
| * d5ed404 8
|/
* 0425411 (tag: originalVersion) 2
* 11fbef7 1
Already a bunch of keystrokes have been spared, and we are optimizing our way of working. Next up, we will add a shortcut to running an external command. In this simple case, we will simply execute ls -al, but it could be an arbitrarily complex command. Note that we add an exclamation mark at the beginning of the alias to signal that it is not a Git command we are running. This can be useful for extending Git. This is, for instance, how Git LFS started. Consider if you would be better off doing a shell alias.
$ git config --local alias.ll '!ls -al'
$ git ll
total 10
drwxr-xr-x 1 joab 1049089   0 Jul  9 13:10 .
drwxr-xr-x 1 joab 1049089   0 Jul  9 13:10 ..
drwxr-xr-x 1 joab 1049089   0 Jul  9 13:14 .git
-rw-r--r-- 1 joab 1049089 155 Jul  9 13:10 gitconfig-alias
-rw-r--r-- 1 joab 1049089  25 Jul  9 13:10 test
So now we have augmented Git’s functionality ever so slightly.
We can all set up scripts to run as in the following section.
$ git config --local alias.helloworld '!f() { echo "Hello World"; }; f'
joab@LT02920 MINGW64 ~/repos/randomsort/practical-git/chapter7/aliases (master)
$ git helloworld
Hello World
And we can make our scripts take arguments.
$ git config --local alias.helloperson '!f() { echo "Hello, ${1}"; }; f'
$ git helloperson Phillip
Hello, Phillip

While these aliases are simple, they should show how powerful a tool they are and how you can both make shortcuts for your often-used commands and extend Git with additional functionality. If you have a common set of things you do in your workflow, you can create aliases for each of these and share them with your team. It is a good way to align on your way of working.

As we have seen, we can quickly create shortcuts for custom commands or even substitute complex parts of our workflow with an alias. Aliases are a massively underused Git feature when it comes to ordinary developers. From now on, you are obligated to create aliases for those things you find yourselves typing out often. You might also once in a while need a complex piece of magic, and the next time you do so, create an alias for it, so it will always be ready at hand.

Attributes

Git attributes are a somewhat advanced part of Git’s feature set. It is one of the places where we can fundamentally change the way Git writes objects in its internal database. They are commonly used to enforce line endings or how to handle binary files, but can also be used to convert to specific coding styles on check-in. As this is something that happens client-side, if we truly want to enforce anything, we need to implement it server-side or in automation engines.

The way we implement attributes is in a .gitignore-like fashion. We create .gitattributes files, and in those, we list paths on which we set and unset attributes on these particular paths. If, for instance, we want to let Git know that a particular XML file is autogenerated and should never be merged like a text file, we can set the attribute binary on it, leading to a .gitattributes like so:
autogeneratedFile.xml binary

Setting the -text attribute on a path stops Git from treating matching paths as text files. The most common scenarios for tweaking existing Git behavior come from either removing the text behavior as shown earlier or forcing Git to treat line endings in a particular way.

We can also use Git attributes to add functionality that is disconnected from what Git would otherwise do. We can do this by adding filters to our configs and reference those filters from our .gitattributes. Git LFS (Git Large File Storage) uses this to handle large files. Filters change how Git handles files going in and how of the repository. Git LFS uploads the matching paths to a central binary repository manager and only saves the reference in Git on check-in. On checkout, Git LFS resolves those paths and downloads the binary files. Git LFS seemingly allows us to store large binary files in Git, which Git is notoriously bad at handling. This reduction in repository size comes at the cost of being able to work fully offline. Not being able to work entirely distributed can be a problem if connectivity is a sparse resource in your context. This filter workflow is shown in Figure 7-2.
../images/495602_1_En_7_Chapter/495602_1_En_7_Fig2_HTML.jpg
Figure 7-2

The clean filter applies when going from the working directory to the staging area and the other direction for smudge

In my experience, Git attributes are rarely necessary unless you have some complexity in your context, such as multiple different platforms on which you check out code using tools that are fragile when it comes to line endings. Of course, the right solution is to fix the fragility or complexity, but until then, Git attributes can help work around the problems.

ATTRIBUTES
In this exercise, we are going to go through a previous kata that generated a merge conflict for us and investigate how we can use .gitattributes to change what happens. In this exercise, we are going to go through the kata merge-mergesort, because we know that will make a merge conflict happen and we can change the outcome of this using Git attributes.
$ cd merge-mergesort
$ . setup.sh
Now, we are in the exercise and we can force the merge conflict by merging in the branch Mergesort-Impl.
$ git merge Mergesort-Impl
Auto-merging mergesort.py
CONFLICT (content): Merge conflict in mergesort.py
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)
Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   mergesort.py
no changes added to commit (use "git add" and/or "git commit -a")
$ cat mergesort.py
from heapq import merge
def merge_sort2(m):
    """Sort list, using two part merge sort"""
    if len(m) <= 1:
        return m
    # Determine the pivot point
    middle = len(m) // 2
    # Split the list at the pivot
<<<<<<< HEAD
    left = m[:middle]
    right = m[middle:]
=======
    right = m[middle:]
    left = m[:middle]
>>>>>>> Mergesort-Impl
    # Sort recursively
    right = merge_sort2(right)
    left = merge_sort2(left)
    # Merge and return
    return list(merge(right, left))
def merge_sort4(m):
    """Sort list, using four part merge sort"""
    if len(m) <= 4:
        return sorted(m)
    # Determine the pivot point
    middle = len(m) // 2
    leftMiddle = middle // 2
    rightMiddle = middle + leftMiddle
    # Split the list at the pivots
    first = m[:leftMiddle]
    second = m[leftMiddle:middle]
    third = m[middle:rightMiddle]
<<<<<<< HEAD
    last = m[rightMiddle:]
=======
    fourth = m[rightMiddle:]
>>>>>>> Mergesort-Impl
    # Sort recursively
    first = merge_sort4(first)
    second = merge_sort4(second)
    third = merge_sort4(third)
<<<<<<< HEAD
    last = merge_sort4(last)
    # Merge and return
    return list(merge(first, second, third, last))
=======
    fourth = merge_sort4(fourth)
    # Merge and return
    return list(merge(first,second, third, fourth))
>>>>>>> Mergesort-Impl
In the preceding code, we notice that there are merge markers. This would have been bad if it had been an autogenerated file or a file where merging doesn’t make any sense. So we abandon the merge.
$ git merge --abort
We then make Git consider mergesort.py a binary file, not to be automatically merged. We then repeat the merge.
$ echo "mergesort.py binary" > .gitattributes
$ git merge Mergesort-Impl
warning: Cannot merge binary files: mergesort.py (HEAD vs. Mergesort-Impl)
Auto-merging mergesort.py
CONFLICT (content): Merge conflict in mergesort.py
Automatic merge failed; fix conflicts and then commit the result.
$ cat mergesort.py
from heapq import merge
def merge_sort2(m):
    """Sort list, using two part merge sort"""
    if len(m) <= 1:
        return m
    # Determine the pivot point
    middle = len(m) // 2
    # Split the list at the pivot
    left = m[:middle]
    right = m[middle:]
    # Sort recursively
    right = merge_sort2(right)
    left = merge_sort2(left)
    # Merge and return
    return list(merge(right, left))
def merge_sort4(m):
    """Sort list, using four part merge sort"""
    if len(m) <= 4:
        return sorted(m)
    # Determine the pivot point
    middle = len(m) // 2
    leftMiddle = middle // 2
    rightMiddle = middle + leftMiddle
    # Split the list at the pivots
    first = m[:leftMiddle]
    second = m[leftMiddle:middle]
    third = m[middle:rightMiddle]
    last = m[rightMiddle:]
    # Sort recursively
    first = merge_sort4(first)
    second = merge_sort4(second)
    third = merge_sort4(third)
    last = merge_sort4(last)
    # Merge and return
    return list(merge(first, second, third, last))
As we can see, we no longer have merge markers in our file but rather have one large self-contained file. We can use git checkout with the flags --ours and --theirs to establish either the incoming file or the one already present in our branch.
$ git checkout --ours -- mergesort.py
$ git add mergesort.py
$ git commit -m “merge”
$ git status

On branch master

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gitattributes

nothing added to commit but untracked files present (use "git add" to track)

So we resolved the merge nicely. If we already know which source we want if there are any conflicts, we can specify that as a merge strategy as a flag to the merge command. First, we reset to the previous stage and then repeat the merge with the strategy flag.
$ git reset --hard HEAD~1
HEAD is now at b4cac37 Mergesort implemented on master
$ git merge --strategy ours Mergesort-Impl
Merge made by the 'ours' strategy.

This exercise showed a simple way to use Git attributes to change the way Git works. There are more advanced things to do with Git attributes, but they are beyond the scope of this book.

Diff and Merge Tools

While the command-line or IDE extensions are enough for most use cases, there are situations where your domain sets you up for some challenging diffs and merges. If this is the case, we can configure Git to use external tools to handle this. Perhaps unsurprising, we set up the tools in git config and can then invoke them through the command line. The process is similar for merge and diff tools. If we have configured a diff tool, we can invoke it through git difftool, and if we have configured a merge tool, the command is git mergetool. There are both free, open source, and proprietary merge tools available. We are using the open source tool meld in the exercise, while a popular paid tool is BeyondCompare. Your team or department might have a preferred tool. If so, it is a good idea to align on that.

MERGE TOOL

This exercise assumes that you have installed the meld merge tool (meldmerge.com) and that you are on Windows. If you are on a different platform, I recommend you follow the platform-specific guides for configuring meld and mergetools, but you will likely have an easier time than those on Windows. First, we will configure meld as the mergetool, and then we will revisit the merge-mergesort kata to see how the merge looks when we use a merge tool to resolve the conflict.

When I installed Meld, it wound up in the path C:Program Files (x86)Meldmeld.exe, so I want to point Git to that.
$ git config --global mergetool.meld.path 'C:Program Files (x86)MeldMeld.exe'
Then, we can tell Git to use Meld as mergetool and difftool.
$ git config --global merge.tool meld
$ git config --global diff.tool meld
So let’s go back to the merge-mergesort kata. Remember to run the setup script again to get a clean kata.
$ pwd
$ . setup.sh
$ git diff Mergesort-Impl
diff --git a/mergesort.py b/mergesort.py
index 9de927a..646b20f 100644
--- a/mergesort.py
+++ b/mergesort.py
@@ -9,8 +9,8 @@ def merge_sort2(m):
     middle = len(m) // 2
     # Split the list at the pivot
-    right = m[middle:]
     left = m[:middle]
+    right = m[middle:]
     # Sort recursively
     right = merge_sort2(right)
@@ -33,13 +33,13 @@ def merge_sort4(m):
     first = m[:leftMiddle]
     second = m[leftMiddle:middle]
     third = m[middle:rightMiddle]
-    fourth = m[rightMiddle:]
+    last = m[rightMiddle:]
     # Sort recursively
     first = merge_sort4(first)
     second = merge_sort4(second)
     third = merge_sort4(third)
-    fourth = merge_sort4(fourth)
+    last = merge_sort4(last)
     # Merge and return
-    return list(merge(first,second, third, fourth))
+    return list(merge(first, second, third, last))
This diff could be useless for more complex products. And we can run meld using the difftool command.
$ git difftool Mergesort-impl
../images/495602_1_En_7_Chapter/495602_1_En_7_Figa_HTML.jpg

Now, we have a more visual view.

Let’s try to move on with the merge.
$ git merge Mergesort-Impl
Auto-merging mergesort.py
CONFLICT (content): Merge conflict in mergesort.py
Automatic merge failed; fix conflicts and then commit the result.
$ git mergetool
Merging:
mergesort.py
Normal merge conflict for 'mergesort.py':
  {local}: modified file
  {remote}: modified file
../images/495602_1_En_7_Chapter/495602_1_En_7_Figb_HTML.jpg

So, we get a visual way of resolving our merges, rather than manually setting the state of the conflicted path.

This can be useful if you work with particular file types or have complex merge conflicts, but I rarely encounter an actual need for these tools in practice. In most cases, the merge conflicts do not appear, and when they do, IDEs come with excellent tool facilitation out of the box.

Hooks

The final configuration option that we cover is Git hooks. Hooks are small shell scripts that allow us to inject functionality in the flow of Git actions. Hooks can help prevent us doing things that we shouldn’t or prepare data for Git.

Hooks are available server-side and client-side. In this book, we only cover client-side hooks, but if you ever notice that a server rejects a push because of non-fast-forward merges, you have seen a server-side hook in action. Other often-used server-side hooks check for a referenced issue or prevent you from accidentally adding large files to your repository.

When it comes to client-side hooks, the same phrase that I’ve used many times is still valid. You can only support workflows client-side if you want to enforce anything you have to do in server-side. Hooks reside in the folder .git/hooks, and when you git init a repository, there is a set of sample hooks that you can check out to see examples of Git hooks in action. If hooks exit with a nonzero exit code, the current action is aborted. We use this in the next exercise to prevent commits on the master branch using the pre-commit hook. In the case of the prepare-commit-msg hook, we can both check for something, that is, the presence of curse words in the commit message or the lack of a referenced issue ID. Thus, hooks help us do the right thing, and through the path of least resistance, we improve. We can, of course, circumvent this locally. Note that hooks are not shared across distributed repositories as this would pose a security issue.

GIT HOOK EXERCISE
In this exercise, we have gone through how to implement a simple hook helping us avoid a common mistake and how to circumvent that hook when we need to. This repository for this exercise can be found in the folder chapter7/pre-commit-hook. If you are on a Mac and experience issues, you can look at this Stack Overflow post for assistance: https://stackoverflow.com/a/14219160/28376.
$ ls
pre-commit*
We can see there is a single file here, but let’s first check that we are able to create a commit in normal fashion.
$ echo "test" > testfile.txt
$ git add testfile.txt
$ git commit -m "Initial commit"
[master (root-commit) 8d6ae42] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 testfile.txt
Nothing surprising here, we could stage a file and create a commit. So let’s look at the content in the file pre-commit. You do not have to be a shell ninja to be able to discern the structure of this script. We exit with an error with the current branch is master; otherwise, we exit with zero. There are a few echo statements to let us see the control flow.
$ cat pre-commit
#!/bin/bash
echo "Running Hook"
if [ `git rev-parse --abbrev-ref HEAD` = master ]
then
    echo "You can't commit to master"
    exit 1
else
    echo "Commit freely on this branch"
fi
Hooks are active by being in the .git/hooks folder and having a name matching when it should run. Our hook is called pre-commit, so it will run before a commit is created.
$ cp pre-commit .git/hooks
With our hook now in place, we will try to see if we can create an additional commit.
$ echo "more content" >> testfile.txt
$ git commit -am "Add more content"
Running Hook
You can't commit to master
Our commit gets rejected, so we will make another branch and create the commit here.
$ git checkout -b other
Switched to a new branch 'other'
$ git commit -am "Add more content"
Running Hook
Commit freely on this branch
[other ec31264] Add more content
 1 file changed, 1 insertion(+)

Our hook runs, but as we are on a different branch, the commit is allowed through. This can be useful to way those oops moments.

But let us say that we really do want to commit on master, even though there is a hook preventing us from doing so. Let’s go back to master and create a commit there.
$ git checkout master
$ echo "some items of interest" > test
$ git add test
$ git commit -m "on master"
Running Hook
You can't commit to master
Our hook is still working and stopping us from committing to master. However, we can prevent the hook from running using the flag --no-verify.
$ git commit --no-verify -am "on master"
[master c6d4486] on master
 1 file changed, 1 insertion(+)
 create mode 100644 test

This is the reason that I have been saying that we need to handle enforcement server-side. One might argue that --no-verify is a bad practice, or couldn’t we just disable it? But consider that the hooks reside in the local repository and there is nothing hindering the user from simply deleting the hook altogether.

At least --no-verify provides us with a proper way to skip the hook.

Katas

To support the learning goals in this chapter, I suggest you practice the following katas:
  • Git-attributes

  • Pre-push

To supplement this, you can go into any local Git repository and look at the sample hooks in the .git/hooks folder.

Summary

In this chapter, we covered many different ways that you can customize your Git installation to work more efficiently and support arbitrary workflows and constraints.

We covered how config files allow us to have global, user, and repository local configurations and how we could use those configurations to extend Git functionality.

We built our own shortcuts and called external commands using aliases. We investigate Git attributes and how we could use them to both tweak Git’s default performance and completely change the base functionality of Git. We covered how you can get a custom merging experience using mergetools. Finally, we covered how we can interfere in the standard Git Flow using hooks to facilitate our workflows.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.12.205