Git Mo' Meta: Easily Adding Meta Information to Git Branches

« Return to Our Notebook

Git Mo' Meta: Easily Adding Meta Information to Git Branches

From time to time, it comes in handy to tie various types of information (ticket id, bug or feature, task owner, sprint information, deadline, etc.) against a branch. Often we can get away with just adding them to the branch name, but it can get ludicrous real fast. In those instances, 'bugfix/jira-613-sprintD-deadline20160523-by_yanick' just doesn't cut it.

Rough-in functionality

The thing is, Git already has a way to attach data to branches. If you look at the .git/config file in one of your repositories, you're likely to find stanzas that look like

[branch "master"]
    remote = origin
    merge = refs/heads/master

[branch "diagram"]
    remote = origin
    merge = refs/heads/master

The keys remote and merge are used by Git itself. And we don't even need to dumpster-dive into the configuration file to look at them:

$ git config --get branch.master.remote
origin

We can also use git config to modify these values. Or — and this is where things get interesting — we can create any other key we want:

# set the value
$ git config branch.diagram.ready-to-share yup

# get the value
$ git config --get branch.diagram.ready-to-share
yup

# new key/value pair is now part of the branch stanza
$ grep -A3 diagram .git/config
[branch "diagram"]
    remote = origin
    merge = refs/heads/master
    ready-to-share = yup

This mechanism has an unexpected bonus: if branches are renamed, the information is automatically carried over:

# rename branch 'diagram' to 'graph'
$ git branch -m graph

$ grep -A3 graph .git/config
[branch "graph"]
    remote = origin
    merge = refs/heads/master
    ready-to-share = yup

Making it nice

So the functionality is there, but it's not terribly user-friendly. That's something that can be easily fixed.

First, to be able to add meta information to a branch, I've written a Git helper script called 'git-meta':

#!/usr/bin/env perl

package App::Git::Meta;

use 5.10.0;

use strict;
use warnings;

use Git::Wrapper;

use Moose;
use MooseX::App::Simple;
use MooseX::MungeHas 'is_ro';

use experimental 'signatures';

has git => sub { Git::Wrapper->new('.') };

option branch => (
    is            => 'ro',
    isa           => 'Str',
    lazy          => 1,
    default       => 'HEAD',
    documentation => 'target branch',
);

parameter key => (
    is       => 'ro',
    required => 1,
);

parameter value => (
    is       => 'ro',
    required => 1,
);

sub run($self) {
    my( $branch ) = $self->git->rev_parse(
        qw/ --abbrev-ref /, $self->branch
    );

    $self->git->config(
        "branch.$branch." . $self->key => $self->value
    );

    say join ' ', $branch, $self->key, $self->value;
}

__PACKAGE__->meta->make_immutable;

__PACKAGE__->new_with_options->run unless caller;

Arguably, bringing in MooseX::App::Simple and Git::Wrapper for such a small script might be overkill. But, then again, they do away with so much mechanical tediousness. It's easy to overlook, because (ironically) the script is so short, but MooseX::App::Simple does away with the pain of parsing the options, and the script's parameters, and generating their defaults, and running the script, and producing the help menu if required. Sure, the script will take a few more milliseconds to run, but those are milliseconds well spent.

In all cases, with that script added to our $PATH, we can now annotate our branches with ease:

# annotate the current branch
$ git meta ready-to-share yup
graph ready-to-share yup

# annotate a different branch
$ git meta --branch=experimental ready-to-share nope
experimental ready-to-share nope

Of course, being able to read it back would be advantageous. So enters a second script, 'git-show-meta':

#!/usr/bin/env perl

package App::Git::ShowMeta;

use 5.10.0;

use Git::Wrapper;

use Moose;

use MooseX::App::Simple;

use MooseX::MungeHas 'is_ro';

use Config::GitLike::Git;
use List::AllUtils qw/ pairgrep pairmap /;
use JSON;
use Data::Printer;

use experimental 'signatures', 'postderef';

has git => sub { Git::Wrapper->new('.') };

option branch => (
    is            => 'ro',
    isa           => 'ArrayRef',
    predicate     => 'has_branch',
    documentation => 'target branches',
);

parameter key_filter   => ( is => 'ro' );
parameter value_filter => ( is => 'ro' );

option format => (
    is      => 'ro',
    isa     => 'Str',
    default => '',
);

has git_config => sub {
    Config::GitLike::Git->new->load('.');
};

sub run($self) {

    my %branches;
    $branches{$_->[0]}{$_->[1]}  = $_->[2] for pairmap {
            [ ( split /\./, $a, 2 ), $b ]
    } pairgrep { $a =~ s/^branch\.// } $self->git_config->%*;

    if ( $self->has_branch ) {
        my %keepers = map {
            $self->git->rev_parse( qw/ --abbrev-ref /, $_ ) => 1
        } $self->branch->@*;

        %branches = pairgrep { $keepers{$a} } %branches;
    }

    if( my $k = $self->key_filter ) {
        %branches = pairmap  { $a => { $k => $b } }
                    pairgrep { $b }
                    pairmap  { $a => $b->{$k} }
                             %branches;

        if( my $v = $self->value_filter ) {
            %branches = pairgrep { $b->{$k} eq $v } %branches;
        }
    }


    if ( $self->format eq 'json' ) {
        say to_json( \%branches, { canonical => 1, pretty => 1 } );
    }
    elsif ( $self->format eq 'column' ) {
        for my $branch ( sort keys %branches ) {
            for my $key ( sort keys $branches{$branch}->%* ) {
                say join ' ', $branch, $key, $branches{$branch}{$key};
            }
        }
    }
    else {
        p %branches, output => 'stdout';
    }
}

__PACKAGE__->meta->make_immutable;

__PACKAGE__->new_with_options->run unless caller;

Again, nothing too esoteric there. We are just querying the Git configuration file for the meta information in a few helpful ways:

# get all meta-information available
$ git show-meta
{
    graph       {
        merge            "refs/heads/master",
        remote           "origin",
        ready-to-share   "yup",
    },
    master      {
        merge    "refs/heads/master",
        remote   "origin"
    },
    experimental        {
        ready-to-share   "nope",
    }
}

# only for some branches
$ git show-meta --branch=experimental --branch=graph
{
    experimental   {
        ready-to-share   "nope",
    },
    graph          {
        merge            "refs/heads/master",
        remote           "origin",
        ready-to-share   "yup",
    }
}

# only show a specific key
$ git show-meta ready-to-share
{
    experimental   {
        ready-to-share   "nope"
    },
    graph          {
        ready-to-share   "yup"
    },
}

# only show a specific value
$ git show-meta ready-to-share yup
{
    graph   {
        ready-to-share   "yup"
    }
}

# show it for scripting pipeline consumption
$ git show-meta --format=json ready-to-share yup
{
"graph" : {
    "ready-to-share" : "yup"
}
}

$ git show-meta --format=column ready-to-share yup
graph ready-to-share yup

# go wild
$ git show-meta --format=column ready-to-share yup \
    | cut -d' ' -f 1 \
    | xargs -IX git push github X

Last words: links & lessons

Quickly, what I hope you took away from this blog entry:

  • Always make your git branch names informative.
  • ... but not informative to the point of clogginess. Instead, for that kind of thing we can leverage the per-branch segments of git's own configuration system.
  • Interacting with that configuration, either via git config or helper scripts is nowhere as scary as one would think.

The git commands discussed in this blog entry are also available in my GitHub environment repository: git-meta and git-show-meta.

Share and enjoy!

We solve problems with technology. What can we solve for you?

Reach Out

t: 800.646.0188