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!