git checkout on bare repo

Hi folks

In this blogpost I tell you how to checkout specific files and folders from a bare repo.
If you cannot imagine a scenario when it may be required see my next blogpost 🙂

For non-bare repos, task is simple

git checkout mybranch folder/file.txt

it gives us a way to get file even if we are on a different branch

But for bare repos, it is more difficult

Let’s try the same command from bare repo, and get the following result

fatal: This operation must be run in a work tree

After googling the issue we can find several approaches

Cloning

git clone . __temp
Push-Location -Path __temp
git checkout origin/mybranch folder/file.txt
Copy-Item -Path folder\file.txt -Destination path\to\destination
Pop-Location
Delete-Item -Path __temp -Recurse -Force

This approach is very inefficient, you have to clone the whole repo, and this can take time and space.

git show or git cat-file

git show master:folder/file.txt > path\to\destination\file.txt

or

git cat-file blob master:folder/file.txt > path\to\destination\file.txt

These approaches would work fine only for text files

And have many problems

  • Encoding. By default “>” operator in PowerShell would write file in UTF-16 encoding.

    Well we can use more verbose syntax with explicit encoding

    git show master:folder/file.txt | Out-File -FilePath path\to\destination\file.txt -Encoding Ascii
    

    This is lame. We had to know desired encoding before hand.

  • Line endings. Result file always has CRLF ending, no matter what ending file actually has.
  • Binary files. Binary file is corrupted.
  • Folders. There is no way to extract whole folder with all files inside.

So, none of these approaches worked.

And then I found one which works perfectly

git archive

It is probably a dirty hack, but it works

git archive mybranch folder/file.txt --output result.tar

It creates a tar archive with desired content, exactly the file that sits in the source control.

It works fine with folders as well.

So we can write a helper

function Git-CheckoutFile
{
    param
    (
        [string] $Branch,
        [string] $RelativeFilePath,
        [string] $DestinationFolder
    )

    if ($RelativeFilePath -eq ".")
    {
        $RelativeFilePath = "*"
    }

    $RelativeFilePath = $RelativeFilePath -replace "\\", "/"

    $tempDir = [Guid]::NewGuid()
    New-Item -Path $tempDir -ItemType Directory | Out-Null

    git archive $Branch $RelativeFilePath --output "$tempDir\__temp.tar"
    tar -xf "$tempDir/__temp.tar" -C $tempDir
    Remove-Item "$tempDir/__temp.tar"

    if (-not (Test-Path $DestinationFolder))
    {
        New-Item -Path $DestinationFolder -ItemType Directory | Out-Null
    }

    Copy-Item -Path "$tempDir/$RelativeFilePath" -Destination $DestinationFolder -Recurse

    Remove-Item -Path $tempDir -Recurse -Force
}

This works as a charm

Git-CheckoutFile -Branch mybranch -RelativeFilePath folder\file.txt -DestinationFolder path\to\destination

Explanation of how it works is pretty simple

We create a tar archive with required files in a temp folder. Then we unpack it.
Then we copy desired files into destination folder.

We cannot unpack straight into destination folder, because archive preserved repo’s tree structure, which we don’t need.

Line #10-13 wrote to support checkout of whole branch using syntax

Git-CheckoutFile -Branch mybranch -RelativeFilePath . -DestinationFolder path\to\destination

We need to change . (dot) into * (asterisk) because otherwise line #29 will produce an incorrect result.

Hope you’ve been as excited as I was 🙂

P.S. According to stackoverflow I should be able to combine lines #20-21 and extract tar file on a fly without creating it.

git archive $Branch $RelativeFilePath | tar -xf -

But this did not work for me

"C:/Program Files (x86)/Git/bin/tar.exe": Malformed extended header: missing newline
"C:/Program Files (x86)/Git/bin/tar.exe": Substituting `.' for empty member name
"C:/Program Files (x86)/Git/bin/tar.exe": .: Cannot open: File exists
"C:/Program Files (x86)/Git/bin/tar.exe": Substituting `.' for empty member name
"C:/Program Files (x86)/Git/bin/tar.exe": .: Cannot open: File exists
"C:/Program Files (x86)/Git/bin/tar.exe": Exiting with failure status due to previous errors

I suspect that this is because of the Windows line endings

Advertisements

About mnaoumov

Senior .NET Developer in Readify
This entry was posted in Uncategorized and tagged , . Bookmark the permalink.

4 Responses to git checkout on bare repo

  1. Pingback: git server-side hooks – maintenance | mnaoumov.NET

  2. The reason piping “git archive” to “tar -xf -” doesn’t work is because PowerShell’s pipe operator is not binary-safe. See http://brianreiter.org/2010/01/29/powershells-object-pipeline-corrupts-piped-binary-data/ for an in-depth analysis to show how and why that is the case.

  3. T says:

    What if you have multiple files?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s