226

I use tabs for indentation in my python programs, but I would like to collaborate (using git) with people who use spaces instead.

Is there a way for git to automatically convert between spaces and tabs (say, 4 spaces = 1 tab) on pushing/fetching? (similar to the CR/LF conversion)

hazzik
  • 13,019
  • 9
  • 47
  • 86
Olivier Verdier
  • 46,998
  • 29
  • 98
  • 90
  • 43
    PEP8 is precisely my problem. Everybody follows it and I'm stuck with my tabs. I happen to think that *one indentation = one tab* is the right thing to do (why spaces? why 4 spaces? PEP8 doesn't explain that...). Anyway, with this git trick, I can happily use tabs on my computer and share my code with all the PEP8 followers out there. – Olivier Verdier Feb 23 '10 at 08:57
  • 8
    Oh! I use TextMate, and I can convert between spaces to tabs. The thing is, when I hit tab, I like my editor to write... tab. So if I checkout a python project with spaces, I will insert all sort of tabs. I must manually convert to tabs, but when I check in, it looks like 1000 deletions, 1000 additions, and my collaborators will not be happy. :-) – Olivier Verdier Feb 23 '10 at 09:45
  • 7
    The reason PEP8 specifies spaces instead of tabs is because of the continuation indentation rules. There are two ways to continue an over-long line inside a parenthetical. If you start a new line immediately after a parenthetical you just indent one. If you instead put part of the content of the parenthetical on the first line then you have to continue the parenthetical on the next line at the indentation level of the opening parenthetical. If you use tabs that doesn't work. – John Christopher Jones Nov 25 '15 at 04:58
  • 3
    @JohnChristopherJones for that situation, one could use tabs to match indentation with the previous line then spaces to match a position in the previous line. This can be converted to spaces easily. Unfortunately the reverse is not true, because it commingles indentation information with alignment information. – Patrick Parker Sep 11 '19 at 15:31

4 Answers4

215

Here is the complete solution:

In your repository, add a file .git/info/attributes which contains:

*.py  filter=tabspace

Linux/Unix

Now run the commands:

git config --global filter.tabspace.smudge 'unexpand --tabs=4 --first-only'
git config --global filter.tabspace.clean 'expand --tabs=4 --initial'

OS X

First install coreutils with brew:

brew install coreutils

Now run the commands:

git config --global filter.tabspace.smudge 'gunexpand --tabs=4 --first-only'
git config --global filter.tabspace.clean 'gexpand --tabs=4 --initial'

All systems

You may now check out all the files of your project. You can do that with:

git checkout HEAD -- **

and all the python files will now have tabs instead of spaces.

Edit: changed the forced checkout command. You should commit your work first, of course.

Marco de Jongh
  • 5,270
  • 3
  • 17
  • 29
Olivier Verdier
  • 46,998
  • 29
  • 98
  • 90
  • @Olivier: nice trice, the `git checkout --force` (quicker than remove and checkout again the relevant files) – VonC Mar 17 '10 at 09:20
  • 1
    The clean filter isn't working for me. When I do git add . I get an error saying "error: external filter expand --tabs=4 --initial failed". I'm on Windows. Does that make a difference? – Jeremy Hicks Jun 09 '11 at 19:06
  • 2
    @Jeremy: expand/unexpand are unix commands. You'll either have to find Windows ports/equivalents or use something like [Cygwin](http://www.cygwin.com/) – Tim Jun 10 '11 at 00:28
  • 1
    I've found bast working version http://sourceforge.net/projects/gnuwin32/files/coreutils/5.3.0/ – hazzik Dec 21 '11 at 14:18
  • 1
    On OS X (snow leopard), `unexpand` and `expand` don't have the same options and `unexpand` appear to be broken (no difference with or without -a option) – Marc-André Lafortune Apr 30 '12 at 22:23
  • 3
    @Marc-André Good point. I actually use the coreutils versions. (Install `homebrew`, and then run `brew install coreutils`). – Olivier Verdier May 01 '12 at 06:32
  • 1
    `git checkout HEAD -- **` gives me errors like `error: pathspec 'name.sublime-project' did not match any file(s) known to git.`. Any ways to get past them? – Kostas Mar 20 '13 at 16:01
  • for anyone confused... to use the commands as referenced... you either need to append `g` to the beginning of both commands - brew leaves the `coreutils` executables prefixed, i.e. `gexpand` - or alias them, somehow.. – Alex Gray Oct 16 '13 at 18:08
  • 1
    Why are the configs global, but the attributes file just in that one repo? Wouldn't it make more sense to put the config in that repo as well, or make everything global? – iconoclast Jul 19 '14 at 00:21
  • @OlivierVerdier, The last step does nothing in my case. Probably because I don't really understand what it is supposed to do. I know that I can `git checkout filename` to undo changes to a certain file. But according to my understanding of your post, `git checkout HEAD -- **` should run through all the files in the repo and apply the filter? Is that correct? – Nimrod Dayan May 26 '16 at 13:10
  • what's the real profit of this scheme? Why not just replace in all files? – dmitryvim Feb 10 '17 at 05:17
  • 2
    It seems that this does not work anymore, the filters do nothing for me. After checkout, the files still have spaces. Any update on this? – Philipp Ludwig May 14 '17 at 13:20
  • 1
    @PhilippLudwig Run `git config filtername` without the `--global` to make sure there isn't a local filter interfering with your global one. – SurpriseDog Aug 01 '21 at 19:29
  • 2
    I also found it easier to just put the attributes file in `~/.config/git/attributes` and the config file in `~/.gitconfig` so these rules apply to all projects instead of messing around with each one. – SurpriseDog Aug 18 '21 at 00:17
152

Yes, one potential solution is to use a git attribute filter driver (see also GitPro book), to define a smudge/clean mechanism.

alt text

That way:

  • each time you checkout some files of your repo, spaces can be converted in tabs,
  • but when you check-in (and push and publish), those same files are stored back using only spaces.

You can declare this filter driver (named here 'tabspace') in the .git/info/attributes (for a filter applied to all files within the Git repo), with the following content:

*.py  filter=tabspace

Now run the commands:

# local config for the current repo
git config filter.tabspace.smudge 'script_to_make_tabs'
git config filter.tabspace.clean 'script_to_make_spaces'

See Olivier's answer for a concrete working example of such a smudge/clean set of instructions.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 1
    Unfortunately, it just doesn't work. I followed all the instructions, but git does not apply the fiter. :-( When I checkout, the smudge filter is not applied, and when I checkin, nothing happens either... git is so frustrating sometimes... – Olivier Verdier Feb 23 '10 at 09:42
  • @Olivier: Strange, I never had any problem with that, as long as I carefully limit the scope of the attribute filter (to a specific subtree, for a specific type of files only) in order to not slow down the checkout/check-in process. See for instance http://stackoverflow.com/questions/62264/dealing-with-svn-keyword-expansion-with-git-svn – VonC Feb 23 '10 at 09:55
  • Thanks! Now it works. See the complete solution: http://stackoverflow.com/questions/2316677/can-git-automatically-switch-between-spaces-and-tabs/2318063#2318063 – Olivier Verdier Feb 23 '10 at 12:33
  • @Vonc: perhaps one should remove the `--global` flag, since this would imply, you send spaces to every collaboration project... – Willem Van Onsem Oct 22 '14 at 13:46
  • @CommuSoft only to the projects which have the right `.gitattributes`. But yes, it is easier to understand if the config is kept local to the repo. I have edited the answer. – VonC Oct 22 '14 at 13:48
45

Very useful info for everyone using GitHub (or other similar service)

~/.gitconfig

[filter "tabspace"]
    smudge = unexpand --tabs=4 --first-only
    clean = expand --tabs=4 --initial
[filter "tabspace2"]
    smudge = unexpand --tabs=2 --first-only
    clean = expand --tabs=2 --initial

Then I have two files: attributes

*.js  filter=tabspace
*.html  filter=tabspace
*.css  filter=tabspace
*.json  filter=tabspace

and attributes2

*.js  filter=tabspace2
*.html  filter=tabspace2
*.css  filter=tabspace2
*.json  filter=tabspace2

Working on personal projects

mkdir project
cd project
git init
cp ~/path/to/attributes .git/info/

That way, when you finally push your work on github, it won't look silly in the code view with 8 space tabs which is default behavior in all browsers.

Contributing to other projects

mkdir project
cd project
git init
cp ~/path/to/attributes2 .git/info/attributes
git remote add origin git@github.com:some/repo.git
git pull origin branch

That way you can work with normal tabs on 2 space indented projects.

Of course you can write similar solution for converting from 4 space to 2 space which is the case if you want to contribute to projects published by me and you tend to use 2 spaces while developing.

Smuuf
  • 6,339
  • 3
  • 23
  • 35
simo
  • 15,078
  • 7
  • 45
  • 59
  • 3
    Related: [Storing git config as part of the repository](https://stackoverflow.com/q/18329621/321973); also note you can use (and commit) a `.gitattributes` file in your repo – Tobias Kienzler May 03 '16 at 12:04
  • Small nit: this has nothing to do with GitHub or any particular service. It's just Git. Nit 2: You probably want to use `$XDG_CONFIG_HOME/git/config` over `$HOME/.gitconfig` to keep your config files organized and your home directory clean. – toastal Dec 15 '22 at 10:54
  • But there's a high chance you want this in `.git` in the project folder as it's not uncommon for projects to have different settings. – toastal Dec 15 '22 at 16:21
1

If you are on windows then you have a few extra steps to get @Olivier Verdier's solution to work.

  1. Download CoreUtils for windows
  2. After installing put the install location in your PATH (How to add a path variable)
  3. I renamed expand.exe to gexpand.exe as there is already a windows expand utility.
odyth
  • 4,324
  • 3
  • 37
  • 45