Presenting RepoManager, a command line interface (CLI) for batch management of multiple Git repositories. RepoManager is available under the MIT license. The Ruby source is located at github.com/robertwahler/repo_manager
RepoManager is a wrapper for Git , the distributed version control system. RepoManager's wrapper functions allow a single Git command to be executed across multiple git repositories.
For example, you have two git repositories named repo1 and repo2 and you want to check the status of both working folders.
cd ~/workspace/delphi/repo1
git status
cd ~/workspace/delphi/repo2
git status
          
repo status
          The RepoManager gem is available on RubyGems.org
gem install repo_manager
          RepoManager's binary is named repo
repo --help
repo --tasks
repo help generate:init
          The remainder of this article will examine a single use case.
Use case: Backup and synchronization of PC save games folders to a central repository (ie Drop Box folder) using Git. Game saves are typically scattered across multiple folders and drives.
This example demonstrates the following features:
The full source of this example is available at github.com/robertwahler/repo_manager/examples
The following commands were used to create this example from scratch
mkdir -p examples/pc_saved_game_backup && cd examples/pc_saved_game_backup
          Create configuration structure with the built-in 'generate:init' task
We are creating a local configuration. For a global configuration, you would execute the init command in your home folder
repo generate:init repo_manager
          Session screenshot
Add a few example save game folders. These folders would normally be scattered over the file system.
mines
mkdir -p saved_games/mines/saves
# profile data will not be stored in the Git repo since it may differ from PC to PC
echo "# dummy profile data" > mines/my_profile.ini
echo "# dummy save" > saved_games/mines/saves/save1
echo "# dummy save" > saved_games/mines/saves/save2
          hearts
mkdir -p saved_games/hearts
echo "# dummy save" > saved_games/hearts/save1
echo "# dummy save" > saved_games/hearts/save2
          This folder will act as a remote to hold bare Git repositories. These repos will store backups of our game saves, normally, this folder would be on a remote server, NAS, or Drop Box like service.
mkdir remote
          User tasks can be added directly to the repo_manager/tasks folder. This task doesn't use any RepoManager specific features, instead, it calls git directly via Thor's run command. Adding the script this way will keep this related functionality with this specific RepoManager configuration. Run repo -T to see a full list of built-in tasks as well as user defined tasks.
    require 'fileutils'
    module RepoManager
      class Generate < Thor
        # full path to the remote folder
        REMOTE = File.expand_path('remote')
        # Create, add, and commit the contents of the current working directory and
        # then push it to a predefined remote folder
        #
        # @example From the repo working
        #
        #   cd ~/my_repo_name
        #   repo generate:remote my_repo_name
        #
        # @example Specify the path to the working folder
        #
        #   repo generate:remote my_repo_name --path=/path/to/my_repo_name
        method_option :remote, :type => :string, :desc => "remote folder or git host, defaults to '#{REMOTE}'"
        method_option :path, :type => :string, :desc => "path to working folder, defaults to CWD"
        desc "remote REPO_NAME", "init a git repo in CWD and push to remote '#{REMOTE}'"
        def remote(name)
          path = options[:path] || FileUtils.pwd
          remote = options[:remote] || "#{File.join(REMOTE, name + '.git')}"
          Dir.chdir path do
            run("git init")
            # core config with windows in mind but works fine on POSIX
            run("git config core.autocrlf false")
            run("git config core.filemode false")
            exit $?.exitstatus if ($?.exitstatus > 1)
            # add everthing and commit
            run("git add .")
            run("git commit --message #{shell_quote('initial commit')}")
            exit $?.exitstatus if ($?.exitstatus > 1)
            # remove old origin first, if it exists
            run("git remote add origin #{remote}")
            run("git config branch.master.remote origin")
            run("git config branch.master.merge refs/heads/master")
            exit $?.exitstatus if ($?.exitstatus > 1)
          end
          run("git clone --bare #{shell_quote(path)} #{remote}")
          exit $?.exitstatus if ($?.exitstatus > 1)
          say "init done on '#{name}'", :green
        end
      end
    end
          In one step, we will initialize a new git repository with the working folder's content and push to a new bare repository for backup.
Normally, you don't need to specify the --path if you are already in the working folder and the repo_manager can find its global config file. For this example, we are using relative paths and will specify the working folder on the command line via the '--path' option.
repo generate:remote mines --path=saved_games/mines/saves
repo generate:remote hearts --path=saved_games/hearts
          
repo add:asset saved_games/mines/saves --name=mines --force
repo add:asset saved_games/hearts --force
          
repo list --short
repo status --unmodified DOTS
          Session screenshot
The following user task will run repo add -A, repo commit, and repo push on all modified repos.
    module RepoManager
      class Action < Thor
        namespace :action
        include Thor::Actions
        include RepoManager::ThorHelper
        class_option :force, :type => :boolean, :desc => "Force overwrite and answer 'yes' to any prompts"
        method_option :repos, :type => :string, :desc => "Restrict update to comma delimited list of repo names", :banner => "repo1,repo2"
        method_option :message, :type => :string, :desc => "Override 'automatic commit' message"
        method_option 'no-push', :type => :boolean, :default => false, :desc => "Force overwrite of existing config file"
        desc "update", "run repo add -A, repo commit, and repo push on all modified repos"
        def update
          initial_filter = options[:repos] ? "--repos=#{options[:repos]}" : ""
          output = run("repo status --short --unmodified=HIDE --no-verbose --no-color #{initial_filter}", :capture => true)
          case $?.exitstatus
            when 0
              say 'no changed repos', :green
            else
              unless output
                say "failed to successfully run 'repo status'", :red
                exit $?.exitstatus
              end
              repos = []
              output = output.split("\n")
              while line = output.shift
                st,repo = line.split("\t")
                repos << repo
              end
              filter = repos.join(',')
              unless options[:force]
                say "Repo(s) '#{filter}' have changed."
                unless ask("Add, commit and push them? (y/n)") == 'y'
                  say "aborting"
                  exit 0
                end
              end
              say "updating #{filter}"
              run "repo add -A --no-verbose --repos #{filter}"
              exit $?.exitstatus if ($?.exitstatus > 1)
              commit_message = options[:message] || "automatic commit @ #{Time.now}"
              run "repo commit --message=#{shell_quote(commit_message)} --no-verbose --repos #{filter}"
              exit $?.exitstatus if ($?.exitstatus > 1)
              unless options['no-push']
                run "repo push --no-verbose --repos #{filter}"
                exit $?.exitstatus if ($?.exitstatus > 1)
              end
              say "update finished", :green
            end
        end
      end
    end
          Only a small subset of non-destructive git commands are enabled by default. We will add the commands needed by our user task to the commands whitelist.
Edit repo.conf and add 'push, add, and commit' to the commands whitelist
    diff --git a/repo_manager/repo.conf b/repo_manager/repo.conf
    index 3cc6dbe..226b8c0 100644
    --- a/repo_manager/repo.conf
    +++ b/repo_manager/repo.conf
    @@ -36,6 +36,9 @@ commands:
     - ls-files
     - show
     - status
    +- push
    +- add
    +- commit
          To view all the available tasks
repo --tasks
          or just
repo -T
          Session screenshot
repo action:update
          Session screenshot
verify working folders are clean, if they are not, either revert them or commit and push
repo status
          pull from remote to all configured repos
repo pull
          
source "http://rubygems.org"
gem "repo_manager"
gem "bundler", ">= 1.0.14"
gem "rspec", ">= 2.6.0"
gem "cucumber", "~> 1.0"
gem "aruba", "= 0.4.5"
gem "win32console", :platforms => [:mingw, :mswin]
          
gem install bundler
cd repo_manager
bundle
          NOTE: This is an excerpt, see the file for the full listing of functional tests
repo_manager/features/tasks/update.feature
@announce
Feature: Automatically commit and update multiple repos
  Background: Test repositories and a valid config file
    Given a repo in folder "test_path_1" with the following:
      | filename         | status | content  |
      | .gitignore       | C      |          |
    And a repo in folder "test_path_2" with the following:
      | filename         | status | content  |
      | .gitignore       | C      |          |
    And a file named "repo.conf" with:
      """
      ---
      folders:
        assets : repo/asset/configuration/files
      """
    And the folder "repo/asset/configuration/files" with the following asset configurations:
      | name    | path         |
      | test1   | test_path_1  |
      | test2   | test_path_2  |
  Scenario: No uncommitted changes
    When I run `repo action:update`
    Then the output should contain:
      """
      no changed repos
      """
  ...
          repo_manager/features/support/steps.rb
    require 'repo_manager/test/base_steps'
    require 'repo_manager/test/asset_steps'
    require 'repo_manager/test/repo_steps'
          repo_manager/features/support/env.rb
    require 'repo_manager'
    require 'aruba/cucumber'
    require 'rspec/expectations'
    Before do
      @aruba_timeout_seconds = 10
    end
    Before('@slow_process') do
      @aruba_io_wait_seconds = 2
    end
          repo_manager/features/support/aruba.rb
    require 'aruba/api'
    require 'fileutils'
    module Aruba
      module Api
        # override aruba avoid 'current_ruby' call and make sure
        # that binary run on Win32 without the binstubs
        def detect_ruby(cmd)
          wrapper = which('repo')
          cmd = cmd.gsub(/^repo/, "ruby -S #{wrapper}") if wrapper
          cmd
        end
      end
    end
          
bundle exec cucumber
          Session screenshots
Handy functions for use under Bash. These work fine on Win32 using Git-Bash.
rpushd: repo pushd (push directory). Wrapper for 'pushd'.
rcd: repo cd (change directory). Wrapper for 'cd', allows for simple cd repo name to the working folder on the filesystem referenced by the 'path' configuration variable.
Source these functions in your .bashrc
function rcd(){ cd "$(repo --match=ONE --no-color path $@)"; }
function rpushd(){ pushd "$(repo path --match=ONE --no-color $@)"; }
alias rpopd="popd"
# provide completion for repo names
function _repo_names()
{
  local cur opts prev
  COMPREPLY=()
  cur="${COMP_WORDS[COMP_CWORD]}"
  opts=`repo list --list=name --no-color`
  COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
  return 0
}
complete -F _repo_names rcd rpushd repo
          For more information, please consult the source: http://github.com/robertwahler/repo_manager.
article comments powered by DisqusCopyright 1999-2013,  GearheadForHire, LLC
GearheadForHire, LLC
      Site design by GearheadForHire, LLC | v2.3.0