Ash Drake

Software Engineer

[email protected]

Twitter Github

Git Folder Structure and Naming Conventions

Recently at work, I ran into one of those once-in-a-while git issues that generally end up being solved with creative Googling and git magic. This time, though, I wanted to understand a little more about how git works.

The problem

I was developing a new feature with a colleague, so we had two branches going that we were rebasing against each other as we went.

feature-name/my-branch
feature-name/his-branch

At some point, we decided it was time to merge our branches into one consolidated effort. At the time, it made sense to me to name the new branch simply feature-name. I attempted to deploy this branch but git on the target machine started throwing errors, saying that

>>> error: 'refs/remotes/origin/feature-name/my-branch' exists; cannot create 'refs/remotes/origin/feature-name'
>>> error: some local refs could not be updated; try running git remote prune origin' to remove any old, conflicting branches

Fortunately, git tells you the solution.

Solution(s)

  • run git prune if you can

Unfortunately, I didn’t have permission to run git while ssh’ed on the target server. I did, however, have permission to edit files, which is where learning a little more about how git works came in handy.

on server:

  • find $PROJECT_DIR/.git/refs/old-branch-name and delete it
  • find $PROJECT_DIR/.git/refs/packed-refs, delete any references here to the old (short name) branch

locally:

  • git checkout -b new branch name which has the same number of path segments as the branch name with the most segments (more below)
  • try deploying/checking out the new branch

All you’re doing is manually scrubbing references to the offending branch, instead of having git prune do it for you.

The real “magic” behind git turns out to simply be git commit hashes. Each one references a snapshot of your working directory at any given time. A commit history is a series of snapshots, and references to a HEAD of a branch or a remote, for example, is really just a reference to a git commit hash.

Try it yourself and see that running the following shows the last commit hash in your master branch. Start at your project directory’s root:

$ cat .git/refs/heads/master

Now, if you run this instead while the master branch is checked out, you’ll see a reference to the file above:

$ cat .git/HEAD

Each branch, remote, or tag is just a point in time in the project’s history, and can be described using git hashes. References, or refs, to these hashes are organized in the .git folder by branch name.

Preventative measures

  • give branch names within a project the same depth of path segments.

The root of the issue was the depth of my branch name. Git lets you name branches like files - including backslashes. Logical conventions for a project with several active branches might be:

fix/feature-being-fixed
feature/new-feature-name
hotfix/thing-that-broke

What this means is that if you have created a file already at $PROJECT/.git/refs/heads/feature/my-branch, then feature is now a folder. Since you can’t create a file at a location that is already a folder, trying to create a new branch named feature results in an attempt to create a file at $PROJECT/.git/refs/heads/feature — but that’s already a directory. Oops!

If you’re set on using the /feature/ path segment, you can always name it feature/main or feature/github_username instead.

What is packed-refs? The packed-refs file acts somewhat like a super directory, and allows for saving space in some large projects. (In my case, deleting the regular ref wasn’t enough since git also looks here.)

Run git pack-refs --all to create this file, which will delete the default ref files in favor of this super-reference. Subsequent ref updates are stored normally, meaning branches that aren’t updated often don’t need to take up space. This can be handy on a project with many contributors, branches, or tags/versions. More here.

Hope this helps someone!

helpful online book: https://git-scm.com