Wednesday, July 4, 2012

Git, Nuget packages and Windows line-endings

The problem

So, you've done your homework and gathered a top set of tools for your web development on a Windows environment: Visual Studio 201x, Git for distributed source control, and a handful of best-of-breed components, i.e.:
  • jQuery and jQuery.UI
  • Bootstrap on 'less' as CSS base framework
  • RequireJs for script loading and dependency handling.
You have added all these third-party components to your solution as Nuget packages.

When setting up your Git repository, you've followed the widely recommended practice for a Windows environment: CRLF conversion to LF when commiting to the repository, and reverse conversion when getting files back into your working area.

So far, so good... or not?

The problems arise when a new version of a package is available and you try to update it.
During the process, the existing package is uninstalled prior to installing the new version, and then a long list of warnings are shown:
Skipping 'Scripts\less.min.js' because it was modified.
Skipping 'Content\less\accordion.less' because it was modified.
Skipping 'Content\less\bootstrap.less' because it was modified.
...

Following that, the installation of the new package fails to update those files:
'Scripts\less.min.js' already exists. Skipping...
'Content\less\accordion.less' already exists. Skipping...
'Content\less\bootstrap.less' already exists. Skipping...
...


Maybe you did modify a few of those files - i.e. 'variables.less' which is meant to be customized - and that is the expected behavior for that file. But for the rest of them, which you never intended to modify, the development workflow is definitely not working.

Lets try to isolate the problem from a fresh start. Assuming you don't have a certain package in your solution (nor any of its files), if you open the Package Manager Console and do:
PM> Install-Package Twitter.Bootstrap.Less -Version 2.0.3
This will add a bunch of files, in this case .less and .js. After that, if you open a Git console and start tracking the new files you'll notice the following:
$ git add .
warning: LF will be replaced by CRLF in ... Scripts\less.min.js
warning: LF will be replaced by CRLF in ... Content\less\accordion.less
...
This smells like trouble... and it is.
If you further commit this changes and then checkout to another branch and then commit back to the current one (causing a refresh of your working folder):
$ git commit -m "Package added, v 2.0.3"
$ git checkout elsewhere
$ git checkout master
This lets your working directory with an altered copy of your original files (CRLFs instead of the original LFs). If you further try to update the package
PM> Update-Package Twitter.Bootstrap.Less -Version 2.0.4
You'll get the ugly warnings shown at the beginning of this post, and none of your files will be updated.
Obviously, this blind "safe behavior" that cannot tell your modified files from the rest is not what you want for your project.

Workaround

When the conflict has already gotten to this point, you have no choice other than:
  1. Uninstall the package
  2. Manually delete each of its remaining files - except for the ones you did willingly modify
  3. Install the new version.
But you may prevent it from happening again on the next update. And this is where you should revisit the decision factors on the line-ending behavior of your Git repository.

 The Right Way   Then what?

In almost every current Windows developer tool, LF-ended lines are no longer a problem.
The shameful exception is Notepad - are you still using it for development tasks? Seriously? Come on...
Notepad++ is one among the many good replacements you can find out there.

In my opinion, if you decide to use certain js / css / less Nuget packages (built for Visual Studio and the Windows environment) because you trust their individual or collective authors, why wouldn't you abide to the line-ending conventions they have chosen?
At a quick glance, all of the components mentioned at the start of this post are using LF line-endings for their text files ( .js, .css, .less)

That's enough motive for me to use the same convention for those types of files. So, while keeping the CRLF line-ending for specific windows-generated text files (i.e. .sln and .csproj files), we are switching our Git repositories to not apply the CRLF to LF and reverse conversions for the extensions .js, .css and .less.

This is done with a .gitattributes file placed at the root of each repository - look at this help file for an explanation of the use of such file and its advantages. For the issue discussed in this post, this is all you need in your .gitattribute file:
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto

# Explicitly declare files we do not want to be converted 
*.js -text
*.css -text
*.less -text
And you are done! All those files will keep their original LF line-endings, both in your working folder and in the repository.

Going further, you may opt to enforce the conversion to LF line-endings for those file types even when you accidentally create a file of your own with CRLFs. This would result in better compliance with the "LF only" recommended practice for Git repositories.

To do that, change the .gitattributes file as follows:
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto

# Explicitly declare files handled as LF-ended (converting them if necessary) 
*.js text=input
*.css text=input
*.less text=input

WARNING: If you apply any of the preceding changes on an existing repository, action must be taken to avoid Git from detecting all affected files as modified on the next commit. You should follow the steps outlined on the Re-normalizing a repo section at the end of the previously mentioned help file.

Acknowledgments

This post from Tim Clem was very enlightening about the handling of line endings in Git. Thanks!

1 comment :

Harry McIntyre said...

Maybe they should just make nuget more tolerant