NAME
App::Multigit - Run commands on a bunch of git repositories without having to deal with git subrepositories.
PACKAGE VARS
%BEHAVIOUR
This holds configuration set by options passed to the mg
script itself.
Observe that mg [options] command [command-options]
will pass options
to mg
, and command-options
to mg-command
. It is those options
that will affect %BEHAVIOUR
.
Scripts may also therefore change %BEHAVIOUR
themselves, but it is probably badly behaved to do so.
report_on_no_output
Defaults to true; this should be used by scripts to determine whether to bother mentioning repositories that gave no output at all for the given task. If you use App::Multigit::Repo::report
, this will be honoured by default.
Controlled by the MG_REPORT_ON_NO_OUTPUT
environment variable.
ignore_stdout
ignore_stderr
These default to false, and will black-hole these streams wherever we have control to do so.
Controlled by the MG_IGNORE_{STDOUT,STDERR}
environment variables.
concurrent_processes
Number of processes to run in parallel. Defaults to 20.
Controlled by the MG_CONCURRENT_PROCESSES
environment variable.
skip_readonly
Do nothing to repositories that have readonly = 1
set in .mgconfig
.
Controlled by the MG_SKIP_READONLY
environment variable.
FUNCTIONS
These are not currently exported.
mgconfig
Returns .mgconfig
. This is a stub to be later configurable, but also to stop me typoing it all the time.
mg_parent
Tries to find the closest directory with an mgconfig
in it. Dies if there is no mgconfig here.
all_repositories
Returns a hashref of all repositories under mg_parent
.
The keys are the repository directories relative to mg_parent
, and the values are the hashrefs from the config, if any.
each($command)
For each configured repository, $command
will be run. Each command is run in a separate process which chdir
s into the repository first.
It returns a convergent App::Multigit::Future that represents all tasks. When this Future completes, all tasks are complete.
Subref form
The most useful form is the subref form. The subref must return a Future; when this Future completes, that repository's operations are done.
The convergent Future ($future
below) completes when all component Futures (the return value of then
, below) have completed. Thus the script blocks at the $future->get
until all repositories have reported completion.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run(\&do_a_thing)
->then($repo->curry::run(\&do_another_thing))
;
});
my @results = $future->get;
See examples/mg-branch
for a simple implementation of this.
The Future can complete with whatever you like, but be aware that run
returns a hash-shaped list; see the docs for run. This means it is often useful for the very last thing in your subref to be a transformation - something that extracts data from the %data
hash and turns it into a usefully-shaped list.
The example examples/mg-closes
does this, whereas examples/mg-branch
uses report
.
report in App::Multigit::Repo implements a sensible directory-plus-output transformation for common usage.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run(\&do_a_thing)
->then($repo->curry::run(\&do_another_thing))
->then($repo->curry::report)
;
});
The subref given to run
is passed the %data
hash from the previous command. %data
is pre-prepared with blank values, so you don't have to check for definedness to avoid warnings, keeping your subrefs nice and clean.
sub do_a_thing {
my ($repo_obj, %data) = @_;
...
}
Thus you can chain them in any order.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run(\&do_another_thing)
->then($repo->curry::run(\&do_a_thing))
->then($repo->curry::report)
;
});
Observe also that the interface to run
allows for the arrayref form as well:
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run([qw/git checkout master/])
->then($repo->curry::run(\&do_another_thing))
;
});
A command may fail. In this case, the Future will fail, and if not handled, the script will die - which is the default behaviour of Future. You can use else to catch this and continue.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run([qw{git rebase origin/master}])
->else([qw{git rebase --abort])
->then($repo->curry::report)
;
});
The failure is thrown in a manner that conforms to the expected Future fail interface, i.e. there is an error message and an error code in there. Following these is the %data
hash that is consistent to all invocations of run
. That means that when you do else
, you should be aware that there will be two extra parameters at the start of the argument list.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run([qw{git rebase origin/master}])
->else(sub {
my ($message, $error, %data) = @_;
...
})
->then($repo->curry::report)
;
});
In the case that you don't care whether the command succeeds or fails, you can use finally to catch the failure and pretend it wasn't actually a failure.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run([qw{git rebase origin/master}])
->finally($repo->curry::report)
;
});
Despite the name, finally
does not have to be the final thing. Think "finally" as in "try/catch/finally". In the following code, finally
simply returns the %data
hash, because finally
transforms a failure into a success and discards the error information.
use curry;
my $future = App::Multigit::each(sub {
my $repo = shift;
$repo
->run([qw{git rebase origin/master}])
->finally(sub { @_ })
->then(\&carry_on_camping)
->then($repo->curry::report)
;
});
Arrayref form
In the arrayref form, the $command
is passed directly to run
in App::Multigit::Repo. The Futures returned thus are collated and the list of return values is thus collated.
Because run completes a Future with a hash-shaped list, the convergent Future that each
returns will be a useless list of all flattened hashes. For this reason it is not actually very much use to do this - but it is not completely useless, because all hashes are the same size:
my $future = App::Multigit::each([qw/git reset --hard HEAD/]);
my @result = $future->get;
my $natatime = List::MoreUtils::natatime(10, @result);
while (my %data = $natatime->()) {
say $data{stdout};
}
However, the %data
hashes do not contain repository information; just the output. It is expected that if repository information is required, the closure form is used.
mg_each
This is the exported name of each
use App::Multigit qw/mg_each/;
init($workdir)
Scans $workdir
for git directories and registers each in .mgconfig
base_branch
Returns the branch that the base repository is on -the repository that contains the .mgconfig
or equivalent.
The purpose of this is to switch the entire project onto a feature branch; scripts can use this as the cue to work against a branch other than master.
This will die if the base repository is not on a branch, because if you've asked for it, giving you a default will more likely be a hindrance than a help.
set_base_branch($branch)
Checks out the provided branch name on the parent repository. Beware of using a branch name that already exists, because this will switch to that branch if it does.
AUTHOR
Alastair McGowan-Douglas, <altreus at perl.org>
ACKNOWLEDGEMENTS
This module could have been a lot simpler but I wanted it to be a foray into the world of Futures. Shout outs go to those cats in irc.freenode.net#perl who basically architectured this for me.
- tm604 (TEAM) - for actually understanding Future architecture, and not being mad at me.
- LeoNerd (PEVANS) - also for not being irritated by my inane questions about IO::Async and Future.
BUGS
Please report bugs on the github repository https://github.com/Altreus/App-Multigit.
LICENSE
Copyright 2015 Alastair McGowan-Douglas.
This program is free software; you can redistribute it and/or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: