Using gitattributes to improve git output

From the git man page:

A gitattributes file is a simple text file that gives attributes to pathnames.

gitattributes allows you to tell git that files should be treated in certain ways. You can use git attributes to apply various attributes but we're focussing on diff. For example, this tells git that files ending in *.ex should be treated as Elixir code during diff operations.

1
*.ex diff=elixir

I couldn't get the standard ~/.gitattributes file location to work for me so I decided to take this opportunity to standardise all my git config under ~/.config/git instead.

TIP You can use git check-attr --all -- path/to/file to check which attributes are applying to a file. This is very useful during setup.

So what effect does this have?

Hunk output

Here's a simple example. On line 5 we can see a change, but that isn't the important part. The interesting difference is what's shown on line 1 after the filename. In this case it's defmodule TestWeb.ListController which in the Elixir module in which the change has been made.

1
2
3
4
5
6
7
8
9
@ path/to/file.ex:25 @ defmodule TestWeb.ListController do
     {:ok, _list} = Lists.delete_list(list)

     conn
+    |> put_flash(:info, "List deleted successfully.")
-    |> put_flash(:info, "List deleted successfully")
     |> redirect(to: Routes.list_path(conn, :index))
   end
 end

If we add *.ex diff=elixir to the attributes file we see def delete(conn, %{"id" => id}), which is the function containing the change. Far more specific.

1
2
3
4
5
6
7
8
9
@ path/to/file.ex:25 @ def delete(conn, %{"id" => id}) do
     {:ok, _list} = Lists.delete_list(list)

     conn
+    |> put_flash(:info, "List deleted successfully.")
-    |> put_flash(:info, "List deleted successfully")
     |> redirect(to: Routes.list_path(conn, :index))
   end
 end

--function-context

Adding this setting also changes the way that --function-context works. What is --function-context? It can be passed to git diff (and other subcommands) and will show the full change in the full context of where the change has been made. By default this means it will show the full module, which is probably not what you want most of the time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ path/to/file.ex:25 @ def delete(conn, %{"id" => id}) do
 defmodule TestWeb.ListController do
   use TestWeb, :controller

   # ...other functions removed for brevity

   def delete(conn, %{"id" => id}) do
     list = Lists.get_list!(id)
     {:ok, _list} = Lists.delete_list(list)

     conn
+    |> put_flash(:info, "List deleted successfully.")
-    |> put_flash(:info, "List deleted successfully")
     |> redirect(to: Routes.list_path(conn, :index))
   end
 end

However, with the change to gitattributes, the "function context" becomes the actual function.

1
2
3
4
5
6
7
8
9
10
11
@ path/to/file.ex:25 @ def delete(conn, %{"id" => id}) do
   def delete(conn, %{"id" => id}) do
     list = Lists.get_list!(id)
     {:ok, _list} = Lists.delete_list(list)

     conn
+    |> put_flash(:info, "List deleted successfully.")
-    |> put_flash(:info, "List deleted successfully")
     |> redirect(to: Routes.list_path(conn, :index))
   end
 end

It's strange that a lot of the available gitattibutes are not defaults in git. But with a few simple tweaks you can get more useful git output.