Home

git

Deleting Git Tags with Style

Deleting Git tags that have already been pushed to your remote is something I have to google literally every time I do it; why the invocation is so arcane, I don’t know. Finally, I decided to automate it with a custom sub-command:

~/.local/bin/git-delete-tag

#!/bin/sh
for tag; do
  git tag -d "$tag"
  git push origin :refs/tags/"$tag"
done

With this script present on $PATH, I can just invoke git delete-tag TAG, .... This is great, but I soon noticed that typing git dele<tab> wouldn’t complete this command (or any custom sub-commands for that matter). After a little digging in the _git completion file, I found the relevant zstyle needed to get this working:

.zshrc

zstyle ':completion:*:*:hub:*' user-commands ${${(M)${(k)commands}:#git-*}/git-/}

Since I’m actually invoking hub, a git wrapper with added functionality for interacting with GitHub, I had to use :hub: in place of :git:, which is what the documentation shows.

I also wanted git delete-tag <tab> to complete with the current tags for the repository. Again, the extension points in the Zsh tab-completion system shine, and it only took a little _git- completion function to make it happen:

.zshrc

_git-delete-tag() { compadd "$@" $(git tag) }

Hopefully this short post will come in useful for Git and Zsh users who, like myself, can never remember how to delete Git tags. As always, you can find the described configuration “in the wild” by way of my dotfiles repo. These items will be within the scripts or zsh tags.

24 Jun 2016, tagged with git, zsh

Git-SVN

If you work on a project that’s been around for a while, chances are it might still be using SVN for version control. Even if you can’t get buy-in from Management or Ops to move to Git, you can still get most of the benefits by learning the ins and outs of the git svn sub command.

Initial Clone

$ git svn clone --stdlayout svn://svn.example.com/project

Using --stdlayout tells Git that your project follows the common layout for trunk, branches, and tags. This allows you to leave off the /trunk when cloning and is important for the branching strategy I’ll mention later.

Alternatively, you could only clone trunk (and have to separately clone every branch or tag you work with) or use separate options to specify the directory structure of your project’s trunk, branches, and tags.

Commits and Branching

Now you can work with the repo as if it were a normal Git repo: commit at will, create and merge local branches, etc. When you want to sync remote changes on trunk to your local master, run:

$ git svn rebase

If you’re unfamiliar with rebasing, you should spend some time reading up on the Git concept itself. Conceptually, Git will remove any local changes you’ve made, sync master with trunk, then reapply your changes over top of the new master.

When you’re satisfied with your local changeset, you can publish your work back to trunk with:

$ git svn dcommit

This will make your local commits one-for-one as SVN commits on trunk. It uses these commits and their messages as-is, so make sure git log shows exactly what you want to publish before you execute this.

Both of these commands require a clean working directory. Later, you’ll see a shortcut for using git stash to get around this limitation.

Branches which you create with a simple git checkout -b foo will be local-only branches. They have no relationship with any SVN branches and you must not try to dcommit or rebase when on these branches.

Master obviously has a relationship to trunk and you can checkout and work with other remote SVN branches similarly with:

$ git checkout -b foo_local foo

Where foo is an SVN branch. Adding the _local suffix is my convention, you can use anything that’s unambiguous.

When on this branch, any rebase or dcommit you do will be interacting with that SVN remote branch.

If branches are added in SVN since your initial clone, you won’t be able to check them out until you let your local repo know about them:

$ git svn fetch

This effectively does a rebase of all branches as part of the update.

At any time, you can a list of all remote branches with:

$ git branch -r

Example Workflow

For non-trivial project work, I’ll typically make a local topic-branch. This allows me to jump back onto master and re-branch if I need to shift gears. It also gives me a chance to fiddle with the commits during the merge to master before I publish it out to SVN.

The simplest case of this would be using a “squash” merge:

$ git checkout -b fix-123
$ ...
$ git commit -am 'fix this thing'
$ ...
$ git commit -am 'fix that other thing'
$ ...
$ git commit -am 'change how I did this or that'
$ ...
$ git commit -am 'add test coverage ;)'

With a branch full of messy commits but a nice clean change-set, I can make all that as one SVN commit:

$ git checkout master
$ git merge --squash fix-123
$ git commit -m 'Fix Issue #123 ...'
$ git svn dcommit

There are more complicated rewrites and rebasings you could do, but this is the workflow I find myself using most often.

Aliases

I’m still coming up with these as needed, but here are a few aliases that should make things easier. Just dump them into a .gitconfig.

[alias]
  sha = svn log --show-commit --oneline -r
  spull = !git stash && git svn rebase && git stash apply
  spush = !git stash && git svn dcommit && git stash apply

The latter two should be fairly obvious, but the first allows you to print the SHA for a given revision; very useful when talking to team members who use plain SVN:

$ git sha 34961
r34961 | b6a9f38 | Fix the thingy #123

16 Jan 2013, tagged with git, svn, workflow, vcs

Git Submodule Config

Git submodules are pretty great. They allow you to have nested git repositories so that modular parts of your app can exist as separate repos but be worked with as one file tree. Another benefit is that when submodules are pushed to github they appear simply as links to the repos they represent.

If your not familiar, go ahead and google then come back – how submodules work overall is not the point of this post.

One of the ways I use submodules is to take modular pieces of my dotfiles repo and separate them out into single purpose, independently clonable repos for oh-my-zsh, vim and screen. A level down, inside the vim submodule itself, I use additional submodules in accordance with tpope’s awesome pathogen plugin to bundle the various vim plugins I use. At both of these levels there exist submodules of which I am the author and an active developer.

When I work on these submodules, I like to do so from within the parent repo (vs independently in some other directory). This is especially important in vim so that I can test out my changes immediately. If I didn’t do this, I would have to hack on the submodule, commit, push, go into the vim repo’s copy and pull – all before seeing the affects (Bret Victor would not be very happy with that workflow).

What this means is the submodule must be added with a pushable remote. And since I like to push using ssh keys and not enter my github credentials each time, I use the git@github url for that. Problem is, when someone wants to clone my repo (that’s what it’s there for), they’re unable to recursively clone those submodules because they don’t have access to them using that url. For that to work, I would’ve had to have added the submodules using the https protocol which allows anonymous checkouts.

As it turns out, due to the unexpected (but perfectly reasonable) behavior of a git submodule add command, I can actually have my cake and eat it too.

You see, when you do a git submodule add <url> <directory>, it writes that url to .gitmodules. This is the file and url that’s used when you clone this repo anywhere else and init the submodules within. But this is not the url that’s used when you actually try to push or pull from within the submodules!

In addition to .gitmodules, the url of the remote also gets written into the .git/config of the submodule as the origin remote (this is just normal clone behavior). This is the url that’s used for push/pull from within the submodule. If you think about it, it makes perfect sense: you’re in a valid git repo; when executing a push, you wouldn’t expect it to use anything but the remote that was defined and stored in your .git/config.

In some versions of git, I find that a submodule’s .git is actually a file pointing to a .git/modules/name/ directory in the parent repo.

Finally, the url/directory mapping for the submodule also gets written into the parent repo’s .git/config. What purpose does that serve? If you figure it out, let me know…

So (however unlikely this is) if you find yourself in the same situation as I, this is how you do that:

$ git submodule add https://github.com/you/repo some/dir
$ git commit -m 'added submodule repo'
$ cd some/dir
$ git remote set-url origin git@github.com:you/repo

Now anyone who clones (recursively) will get the anonymous checkout version (as defined in .gitmodules), but the origin remote in the local submodule has been changed to the git@github version and is pushable using ssh keys.

I recently discovered that this can be solved much more elegantly by adding the following to ~/.gitconfig:

[url "git@github.com:pbrisbin/"]
  pushInsteadOf = "https://github.com/pbrisbin/"

Now whenever git encounters the anonymous http remote, it’ll silently use the ssh-based url. Aces.

27 Apr 2012, tagged with git, linux