Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

SCM Syncer: SVN and Git #276

Closed
wants to merge 2 commits into from

9 participants

@kikito

Hi there!

Please find attached a pull request that adds support for backing up SCMs (both with Git and SVN) into Backup.

I have tried to mimic the changes you made to my previous pull request (RSync::Push-Pull-etc). For example, this version uses a "repositories" property (alias for "directories") like so:

Backup::Model.new(:hdl_make, 'Description for hdl_make') do
  sync_with SCM::Git do |git|
    git.ip           = "example.com"
    git.path         = "backups/git"
    git.repositories do
      add "/misc/repo-1.git"
      add "/foo/bar/repo-2.git"
    end
  end
end

This will backup the repositories git://example.com/misc/repo-1.git and git://example.com/foo/bar/repo-2.git inside the folder backups/git.

Best regards,

Enrique

@kikito

Hi,

I detected a small issue in 1.8.7 - apologies! Test added and fix submitted.

@meskyanichi
Owner

Looks good. What do you think @burns? Also, do you feel that any of the other pull requests should be merged in before a new gem release?

@englishm

Wondering if there are any plans to add SVN/Git support to the backup gem (like this pull req, or otherwise)...

I monkey-patched a custom provider for our internal git hosting servers to push the bare repositories to additional remotes on a backup server. Unfortunately I wrote it against 3.0.19 and it seems that 9be16f7 reorganized things substantially such that my monkey-patching no longer works with newer versions of the gem.

@petemounce

Why svnsync instead of svnadmin hotcopy? I don't know the semantics of the former, but the latter allows the repository to be active during the backup; that is, it won't lose transactions in-flight. But, it's been a long time since I looked at backing up svn repositories; I'm probably out of date.

I'm not certain why we used svnsync, but I'm sure we knew svnadmin existed and yet we used svnsync. But it has been years since I have had to use these, I'm afraid I have forgotten the details. I'd venture it has to do with the permissions required for each action. But that's a guess.

I've skimmed some docs; maybe svnsync because it can operate remotely, whereas svnadmin hotcopy needs to operate locally? Flexibility I guess...? Except there are file:// URLs there, note remote ones. Shrug.

@petemounce

I'd really like for this to get merged; I discovered backups (!) recently, and I wanted exactly this, to be able to back up svn repositories.

@guilhem

:+1:
Adding mercurial (Hg) will be perfect :)

@assimovt

Looks solid. :+1:

@assimovt

What should be done to get this pull request merged?

@kuraga

+1

@tombruijn tombruijn added the Discuss label
@tombruijn
Owner

The problem with this PR is that is has gone untouched for too long. It's from Feb 16, 2012 and all of its changes are based on the 3.0.2x version it branched off from, the 3.0 version is now at 3.11.0. I will close this PR as it is now unmergable. If someone wants to re-add this feature (for 3.0 or 4.0) please submit a new PR and we will take a look at it sooner.

@tombruijn tombruijn closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  README.md
@@ -88,6 +88,7 @@ Below you find a list of components that Backup currently supports. If you'd lik
- RSync (Push, Pull and Local)
- Amazon S3
- Rackspce Cloud Files
+- SCM support (SVN and Git)
[Syncer Wiki Page](https://github.com/meskyanichi/backup/wiki/Syncers)
View
18 lib/backup.rb
@@ -72,6 +72,11 @@ module RSync
autoload :Push, File.join(SYNCER_PATH, 'rsync', 'push')
autoload :Pull, File.join(SYNCER_PATH, 'rsync', 'pull')
end
+ module SCM
+ autoload :Base, File.join(SYNCER_PATH, 'scm', 'base')
+ autoload :Git, File.join(SYNCER_PATH, 'scm', 'git')
+ autoload :SVN, File.join(SYNCER_PATH, 'scm', 'svn')
+ end
end
##
@@ -164,11 +169,16 @@ module Syncer
autoload :Cloud, File.join(CONFIGURATION_PATH, 'syncer', 'cloud')
autoload :CloudFiles, File.join(CONFIGURATION_PATH, 'syncer', 'cloud_files')
autoload :S3, File.join(CONFIGURATION_PATH, 'syncer', 's3')
+ module SCM
+ autoload :Base, File.join(CONFIGURATION_PATH, 'syncer', 'scm', 'base')
+ autoload :Git, File.join(CONFIGURATION_PATH, 'syncer', 'scm', 'git')
+ autoload :SVN, File.join(CONFIGURATION_PATH, 'syncer', 'scm', 'svn')
+ end
module RSync
- autoload :Base, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'base')
- autoload :Local, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'local')
- autoload :Push, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'push')
- autoload :Pull, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'pull')
+ autoload :Base, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'base')
+ autoload :Local, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'local')
+ autoload :Push, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'push')
+ autoload :Pull, File.join(CONFIGURATION_PATH, 'syncer', 'rsync', 'pull')
end
end
View
6 lib/backup/config.rb
@@ -111,7 +111,11 @@ def add_dsl_constants!
# Encryptors
['OpenSSL', 'GPG'],
# Syncers
- ['Rackspace', 'S3', { 'RSync' => ['Push', 'Pull', 'Local'] }],
+ [ 'Rackspace',
+ 'S3',
+ { 'SCM' => ['SVN', 'Git'] },
+ { 'RSync' => ['Push', 'Pull', 'Local'] }
+ ],
# Notifiers
['Mail', 'Twitter', 'Campfire', 'Presently', 'Prowl', 'Hipchat']
]
View
17 lib/backup/configuration/syncer/scm/base.rb
@@ -0,0 +1,17 @@
+# encoding: utf-8
+
+module Backup
+ module Configuration
+ module Syncer
+ module SCM
+ class Base < Syncer::Base
+ class << self
+
+ attr_accessor :protocol, :username, :password, :ip, :port, :path, :additional_options
+
+ end
+ end
+ end
+ end
+ end
+end
View
12 lib/backup/configuration/syncer/scm/git.rb
@@ -0,0 +1,12 @@
+# encoding: utf-8
+
+module Backup
+ module Configuration
+ module Syncer
+ module SCM
+ class Git < Base
+ end
+ end
+ end
+ end
+end
View
12 lib/backup/configuration/syncer/scm/svn.rb
@@ -0,0 +1,12 @@
+# encoding: utf-8
+
+module Backup
+ module Configuration
+ module Syncer
+ module SCM
+ class SVN < Base
+ end
+ end
+ end
+ end
+end
View
2  lib/backup/model.rb
@@ -128,7 +128,7 @@ def sync_with(name, &block)
# Warn user of DSL change from 'RSync' to 'RSync::Local'
if name.to_s == 'Backup::Config::RSync'
Logger.warn Errors::ConfigError.new(<<-EOS)
- Configuration Update Needed for Syncer::RSync
+ Config Update Needed for Syncer::RSync
The RSync Syncer has been split into three separate modules:
RSync::Local, RSync::Push and RSync::Pull
Please update your configuration for your local RSync Syncer
View
84 lib/backup/syncer/scm/base.rb
@@ -0,0 +1,84 @@
+# encoding: utf-8
+
+module Backup
+ module Syncer
+ module SCM
+ class Base < Syncer::Base
+
+ ##
+ # In a server url such as:
+ # https://jimmy:password@example.com:80
+ # -> [protocol]://[username]:[password][ip]:[port]
+ attr_accessor :protocol, :username, :password, :ip, :port
+
+ ##
+ # repositories is an alias to directories; provided just for clarity
+ alias :repositories :directories
+ alias :repositories= :directories=
+
+ ##
+ # Instantiates a new Repository Syncer object
+ # and sets the default configuration
+ def initialize
+ load_defaults!
+
+ @path ||= 'backups'
+ @directories = Array.new
+ end
+
+ def perform!
+ Logger.message("#{ self.class } started syncing '#{ path }'.")
+ repositories.each do |repository|
+ backup_repository! repository
+ end
+ end
+
+ def authority
+ "#{protocol}://#{credentials}#{ip}#{prefixed_port}"
+ end
+
+ def repository_urls
+ repositories.collect{ |r| repository_url(r) }
+ end
+
+ def repository_url(repository)
+ "#{authority}#{repository}"
+ end
+
+ def repository_local_path(repository)
+ File.join(path, repository)
+ end
+
+ def repository_absolute_local_path(repository)
+ if File.respond_to? :absolute_path # ruby 1.9.x
+ File.absolute_path(repository_local_path(repository))
+ else #ruby 1.8.7
+ File.expand_path(repository_local_path(repository))
+ end
+ end
+
+ def repository_local_container_path(repository)
+ File.dirname(repository_local_path(repository))
+ end
+
+ def create_repository_local_container!(repository)
+ FileUtils.mkdir_p(repository_local_container_path(repository))
+ end
+
+ private
+
+ def credentials
+ "#{username}#{prefixed_password}@" if username
+ end
+
+ def prefixed_password
+ ":#{password}" if password
+ end
+
+ def prefixed_port
+ ":#{port}" if port
+ end
+ end
+ end
+ end
+end
View
49 lib/backup/syncer/scm/git.rb
@@ -0,0 +1,49 @@
+# encoding: utf-8
+
+module Backup
+ module Syncer
+ module SCM
+ class Git < Base
+
+ attr_accessor :username, :password, :host, :protocol, :port, :repo_path, :path
+
+ def initialize(&block)
+ super
+
+ @protocol ||= 'git'
+
+ instance_eval(&block) if block_given?
+ end
+
+ def local_repository_exists?(repository)
+ local_path = repository_local_path(repository)
+ run "cd #{local_path} && git rev-parse --git-dir > /dev/null 2>&1"
+ return true
+ rescue Errors::CLI::SystemCallError
+ Logger.message("#{local_path} is not a repository")
+ return false
+ end
+
+ def clone_repository!(repository)
+ Logger.message("Cloning repository in '#{repository_local_path(repository)}'.")
+ create_repository_local_container!(repository)
+ run "cd #{repository_local_container_path(repository)} && git clone --bare #{repository_url(repository)}"
+ end
+
+ def update_repository!(repository)
+ local_path = repository_local_path(repository)
+ Logger.message("Updating repository in '#{local_path}'.")
+ run "cd #{local_path} && git fetch --all"
+ end
+
+ def backup_repository!(repository)
+ if local_repository_exists?(repository)
+ update_repository!(repository)
+ else
+ clone_repository!(repository)
+ end
+ end
+ end
+ end
+ end
+end
View
56 lib/backup/syncer/scm/svn.rb
@@ -0,0 +1,56 @@
+# encoding: utf-8
+
+module Backup
+ module Syncer
+ module SCM
+ class SVN < Base
+
+ def initialize(&block)
+
+ super
+
+ @protocol ||= "http"
+
+ instance_eval(&block) if block_given?
+ end
+
+ def local_repository_exists?(repository)
+ run "svnadmin verify #{repository_local_path(repository)}"
+ return true
+ rescue Errors::CLI::SystemCallError
+ return false
+ end
+
+ def initialize_repository!(repository)
+ local_path = repository_local_path(repository)
+ absolute_path = repository_absolute_local_path(repository)
+ url = repository_url(repository)
+ hook_path = File.join(local_path, 'hooks', 'pre-revprop-change')
+
+ Logger.message("Initializing empty svn repository in '#{local_path}'.")
+
+ create_repository_local_container!(repository)
+
+ run "svnadmin create '#{local_path}'"
+ run "echo '#!/bin/sh' > '#{hook_path}'"
+ run "chmod +x '#{hook_path}'"
+ run "svnsync init file://#{absolute_path} #{url}"
+ end
+
+ def update_repository!(repository)
+ absolute_path = repository_absolute_local_path(repository)
+ local_path = repository_local_path(repository)
+
+ Logger.message("Updating svn repository in '#{local_path}'.")
+ run("svnsync sync file://#{absolute_path} --non-interactive")
+ end
+
+
+ def backup_repository!(repository)
+ initialize_repository!(repository) unless local_repository_exists?(repository)
+ update_repository!(repository)
+ end
+ end
+ end
+ end
+end
View
2  spec/cli/utility_spec.rb
@@ -188,7 +188,7 @@
[--config-path=CONFIG_PATH] # Path to your Backup configuration directory
[--databases=DATABASES] # (mongodb, mysql, postgresql, redis, riak)
[--storages=STORAGES] # (cloud_files, dropbox, ftp, local, ninefold, rsync, s3, scp, sftp)
- [--syncers=SYNCERS] # (cloud_files, rsync_local, rsync_pull, rsync_push, s3)
+ [--syncers=SYNCERS] # (cloud_files, rsync_local, rsync_pull, rsync_push, s3, scm_git, scm_svn)
[--encryptors=ENCRYPTORS] # (gpg, openssl)
[--compressors=COMPRESSORS] # (bzip2, gzip, lzma, pbzip2)
[--notifiers=NOTIFIERS] # (campfire, hipchat, mail, presently, prowl, twitter)
View
48 spec/configuration/syncer/scm/base_spec.rb
@@ -0,0 +1,48 @@
+# encoding: utf-8
+
+require File.expand_path('../../../../spec_helper.rb', __FILE__)
+
+describe Backup::Configuration::Syncer::SCM do
+ before do
+ Backup::Configuration::Syncer::SCM::Base.defaults do |default|
+ default.protocol = 'http'
+ default.username = 'my_user_name'
+ default.password = 'secret'
+ default.ip = 'example.com'
+ default.port = 1234
+ default.path = '~/backups/'
+ default.additional_options = 'some_additional_options'
+ # base.directories/repositories # can not have a default value
+
+ end
+ end
+
+ after { Backup::Configuration::Syncer::SCM::Base.clear_defaults! }
+
+ it 'should set the default base configuration' do
+ base = Backup::Configuration::Syncer::SCM::Base
+ base.protocol.should == 'http'
+ base.username.should == 'my_user_name'
+ base.password.should == 'secret'
+ base.ip.should == 'example.com'
+ base.port.should == 1234
+ base.path.should == '~/backups/'
+ base.additional_options == 'some_additional_options'
+ end
+
+ describe '#clear_defaults!' do
+ it 'should clear all the defaults, resetting them to nil' do
+ Backup::Configuration::Syncer::SCM::Base.clear_defaults!
+
+ base = Backup::Configuration::Syncer::SCM::Base
+ base.protocol.should == nil
+ base.username.should == nil
+ base.password.should == nil
+ base.ip.should == nil
+ base.port.should == nil
+ base.path.should == nil
+ base.additional_options == nil
+ end
+ end
+
+end
View
10 spec/configuration/syncer/scm/git_spec.rb
@@ -0,0 +1,10 @@
+# encoding: utf-8
+
+require File.expand_path('../../../../spec_helper.rb', __FILE__)
+
+describe Backup::Configuration::Syncer::SCM::Git do
+ it 'should be a subclass of SCM::Base' do
+ git = Backup::Configuration::Syncer::SCM::Git
+ git.superclass.should == Backup::Configuration::Syncer::SCM::Base
+ end
+end
View
10 spec/configuration/syncer/scm/svn_spec.rb
@@ -0,0 +1,10 @@
+# encoding: utf-8
+
+require File.expand_path('../../../../spec_helper.rb', __FILE__)
+
+describe Backup::Configuration::Syncer::SCM::SVN do
+ it 'should be a subclass of SCM::Base' do
+ svn = Backup::Configuration::Syncer::SCM::SVN
+ svn.superclass.should == Backup::Configuration::Syncer::SCM::Base
+ end
+end
View
82 spec/storage/local_spec.rb.orig
@@ -0,0 +1,82 @@
+# encoding: utf-8
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Backup::Storage::Local do
+
+ let(:local) do
+ Backup::Storage::Local.new do |local|
+ local.path = '~/backups/'
+ local.keep = 20
+ end
+ end
+
+ before do
+ Backup::Configuration::Storage::Local.clear_defaults!
+ end
+
+ it 'should have defined the configuration properly' do
+ local.path.should == "#{ENV['HOME']}/backups/"
+ local.keep.should == 20
+ end
+
+ it 'should use the defaults if a particular attribute has not been defined' do
+ Backup::Configuration::Storage::Local.defaults do |local|
+ local.path = '~/backups'
+ end
+
+ local = Backup::Storage::Local.new do |local|
+ local.path = '~/my-backups'
+ end
+
+ local.path.should == "#{ENV['HOME']}/my-backups"
+ end
+
+ it 'should have its own defaults' do
+ local = Backup::Storage::Local.new
+ local.path.should == "#{ENV['HOME']}/backups"
+ end
+
+ describe '#transfer!' do
+ before do
+ local.stubs(:create_local_directories!)
+ end
+
+ it 'should transfer the provided file to the path' do
+ Backup::Model.new('blah', 'blah') {}
+
+ local.expects(:create_local_directories!)
+
+ FileUtils.expects(:cp).with(
+ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar"),
+ File.join("#{ENV['HOME']}/backups/myapp", "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar")
+ )
+
+ local.send(:transfer!)
+ end
+ end
+
+ describe '#remove!' do
+ it 'should remove the file from the remote server path' do
+ FileUtils.expects(:rm).with("#{ENV['HOME']}/backups/myapp/#{ Backup::TIME }.#{ Backup::TRIGGER }.tar")
+ local.send(:remove!)
+ end
+ end
+
+ describe '#create_remote_directories!' do
+ it 'should properly create remote directories one by one' do
+ local.path = "#{ENV['HOME']}/backups/some_other_folder/another_folder"
+ FileUtils.expects(:mkdir_p).with("#{ENV['HOME']}/backups/some_other_folder/another_folder/myapp")
+ local.send(:create_local_directories!)
+ end
+ end
+
+ describe '#perform' do
+ it 'should invoke transfer! and cycle!' do
+ local.expects(:transfer!)
+ local.expects(:cycle!)
+ local.perform!
+ end
+ end
+
+end
View
182 spec/syncer/scm/base_spec.rb
@@ -0,0 +1,182 @@
+# encoding: utf-8
+
+require File.expand_path('../../../spec_helper.rb', __FILE__)
+
+describe Backup::Syncer::SCM::Base do
+ let(:syncer) { Backup::Syncer::SCM::Base.new }
+
+ describe '#initialize' do
+
+ it 'should use default values' do
+ syncer.path.should == 'backups'
+ syncer.repositories.should == []
+ end
+
+ context 'when setting configuration defaults' do
+ after { Backup::Configuration::Syncer::SCM::Base.clear_defaults! }
+
+ it 'should use the configured defaults' do
+ Backup::Configuration::Syncer::SCM::Base.defaults do |default|
+ default.path = 'some_path'
+ #default.directories = 'cannot_have_a_default_value'
+ end
+ syncer = Backup::Syncer::SCM::Base.new
+ syncer.path.should == 'some_path'
+ syncer.repositories.should == []
+ end
+ end
+
+ end # describe '#initialize'
+
+ describe '#repositories' do
+ before do
+ syncer.repositories = ['/some/repo.git', '/a/svn/repo']
+ end
+
+ context 'when no block is given' do
+ it 'should return repositories' do
+ syncer.repositories.should ==
+ ['/some/repo.git', '/a/svn/repo']
+ end
+ end
+
+ context 'when a block is given' do
+ it 'should evalute the block, allowing #add to add directories' do
+ syncer.repositories do
+ add '/a/new/repo.git'
+ add '/another/new/repo.git'
+ end
+ syncer.repositories.should == [
+ '/some/repo.git',
+ '/a/svn/repo',
+ '/a/new/repo.git',
+ '/another/new/repo.git'
+ ]
+ end
+ end
+ end # describe '#repositories'
+
+ describe '#add' do
+ before do
+ syncer.repositories = ['/some/repo.git', '/another/repo.git']
+ end
+
+ it 'should add the given path to repositories' do
+ syncer.add '/yet/another/repo.git'
+ syncer.repositories.should ==
+ ['/some/repo.git', '/another/repo.git', '/yet/another/repo.git']
+ end
+ end
+
+ context 'when handling fully-qualified repositories' do
+
+ before do
+ syncer.protocol = 'http'
+ syncer.username = 'jimmy'
+ syncer.password = 'secret'
+ syncer.ip = 'example.com'
+ syncer.port = '80'
+ end
+
+ describe '#authority' do
+
+ it "gets calculated using protocol, host, port and path" do
+ syncer.authority.should == "http://jimmy:secret@example.com:80"
+ end
+
+ it "ignores missing port successfully" do
+ syncer.port = nil
+ syncer.authority.should == "http://jimmy:secret@example.com"
+ end
+
+ it "ignores missing user successfully" do
+ syncer.username = nil
+ syncer.authority.should == "http://example.com:80"
+ end
+
+ it "ignores missing password successfully" do
+ syncer.password = nil
+ syncer.authority.should == "http://jimmy@example.com:80"
+ end
+ end
+
+ describe "#repository_url" do
+ it "given a repository, it returns the local path where it will be stored" do
+ syncer.repository_url('/my/repo.git').should == "http://jimmy:secret@example.com:80/my/repo.git"
+ end
+ end
+
+ describe "#repository_urls" do
+ it "returns the repositories prefixed with the authority" do
+ syncer.repositories do
+ add '/my/repo.git'
+ add '/my/other/repo.git'
+ end
+ syncer.repository_urls.should == [
+ "http://jimmy:secret@example.com:80/my/repo.git",
+ "http://jimmy:secret@example.com:80/my/other/repo.git"
+ ]
+ end
+ end
+ end
+
+ describe "#repository_local_path" do
+ it "returns the path of a given repository" do
+ syncer.repository_local_path("/my/repo.git").should == "backups/my/repo.git"
+ end
+ end
+
+ describe "#repository_absolute_local_path" do
+ let(:result) { "/home/jimmy/backups/my/repo" }
+
+ it "returns the absolute path of a given repository (ruby 1.9.x)" do
+ File.expects(:respond_to?).with(:absolute_path).returns(true)
+ File.expects(:absolute_path).with("backups/my/repo").returns(result)
+ syncer.repository_absolute_local_path("/my/repo").should == result
+ end
+
+ it "returns the absolute path of a given repository (ruby 1.8.7)" do
+ File.expects(:respond_to?).with(:absolute_path).returns(false)
+ File.expects(:expand_path).with("backups/my/repo").returns(result)
+ syncer.repository_absolute_local_path("/my/repo").should == result
+ end
+ end
+
+ describe "#repository_local_container_path" do
+ it "returns the path of the directory that will contain the repo locally" do
+ syncer.repository_local_container_path("/my/deep/repo.git").should == "backups/my/deep"
+ end
+ end
+
+ describe "#create_repository_local_container!" do
+ it "creates the container if needed" do
+ FileUtils.stubs(:mkdir_p)
+ FileUtils.expects(:mkdir_p).with("backups/my/deep")
+ syncer.create_repository_local_container!("/my/deep/repo.git")
+ end
+ end
+
+ describe "#perform!" do
+ it "invokes update_repository once per each repository" do
+ syncer.stubs(:backup_repository!)
+
+ syncer.repositories do
+ add '/my/repo.git'
+ add '/my/other/repo.git'
+ end
+
+ Backup::Logger.expects(:message).with("Backup::Syncer::SCM::Base started syncing 'backups'.")
+ syncer.expects(:backup_repository!).with('/my/repo.git')
+ syncer.expects(:backup_repository!).with('/my/other/repo.git')
+
+ syncer.perform!
+ end
+ end
+
+ describe "#backup_repository!" do
+ it "throws an error when invoked" do
+ lambda { syncer.backup_repository('my/repo.git') }.should raise_error
+ end
+ end
+
+end
View
80 spec/syncer/scm/git_spec.rb
@@ -0,0 +1,80 @@
+# encoding: utf-8
+require File.expand_path('../../../spec_helper.rb', __FILE__)
+
+describe Backup::Syncer::SCM::Git do
+
+ let(:git) do
+ Backup::Syncer::SCM::Git.new do |git|
+ git.ip = 'example.com'
+ git.repositories do
+ add '/a/repo.git'
+ add '/another/repo.git'
+ end
+ end
+ end
+
+ describe '#initialize' do
+ it 'should use default values' do
+ git.protocol == 'git'
+ git.path.should == 'backups'
+ git.repositories.should == ['/a/repo.git', '/another/repo.git']
+ end
+ end
+
+
+
+ it 'should be a subclass of SCM::Base' do
+ Backup::Syncer::SCM::Git.superclass.should == Backup::Syncer::SCM::Base
+ end
+
+ describe '#local_repository_exists?' do
+ let(:command) { "cd backups/my_repo && git rev-parse --git-dir > /dev/null 2>&1" }
+ it "returns false when not in a working copy" do
+ git.expects(:run).with(command).raises(Backup::Errors::CLI::SystemCallError)
+ git.local_repository_exists?("my_repo").should be_false
+ end
+
+ it "returns true when inside a working copy" do
+ git.expects(:run).with(command)
+ git.local_repository_exists?("my_repo").should be_true
+ end
+ end
+
+ describe '#clone_repository!' do
+ it 'initializes an empty repository' do
+ Backup::Logger.expects(:message).with("Cloning repository in 'backups/my/repo.git'.")
+ git.expects(:create_repository_local_container!).with('/my/repo.git')
+ git.expects(:run).with("cd backups/my && git clone --bare git://example.com/my/repo.git")
+
+ git.clone_repository!('/my/repo.git')
+ end
+ end
+
+ describe '#update_repository!' do
+ it 'invokes git fetch' do
+ Backup::Logger.expects(:message).with("Updating repository in 'backups/my/repo.git'.")
+ git.expects(:run).with("cd backups/my/repo.git && git fetch --all")
+
+ git.update_repository!('/my/repo.git')
+ end
+ end
+
+ describe '#backup_repository!' do
+ context 'when the local repository exists' do
+ it 'invokes update_repository!' do
+ git.expects(:local_repository_exists?).with('/my/repo.git').returns(true)
+ git.expects(:update_repository!).with('/my/repo.git')
+
+ git.backup_repository!('/my/repo.git')
+ end
+ end
+ context 'when the local repository does not exist' do
+ it 'invokes clone_repository!' do
+ git.expects(:local_repository_exists?).with('/my/repo.git').returns(false)
+ git.expects(:clone_repository!).with('/my/repo.git')
+
+ git.backup_repository!('/my/repo.git')
+ end
+ end
+ end
+end
View
94 spec/syncer/scm/svn_spec.rb
@@ -0,0 +1,94 @@
+# encoding: utf-8
+
+require File.expand_path('../../../spec_helper.rb', __FILE__)
+
+describe Backup::Syncer::SCM::SVN do
+
+ let(:svn) do
+ Backup::Syncer::SCM::SVN.new do |svn|
+ svn.ip = 'example.com'
+ svn.repositories do
+ add '/a/repo/trunk'
+ add '/another/repo/trunk'
+ end
+ end
+ end
+
+ describe '#initialize' do
+ it 'should use default values' do
+ svn.protocol == 'http'
+ svn.path.should == 'backups'
+ svn.repositories.should == ['/a/repo/trunk', '/another/repo/trunk']
+ end
+ end
+
+ it 'should be a subclass of SCM::Base' do
+ Backup::Syncer::SCM::SVN.superclass.should == Backup::Syncer::SCM::Base
+ end
+
+
+ describe '#local_repository_exists?' do
+ it "returns false when not in a working copy" do
+ svn.expects(:run).with('svnadmin verify backups/my_repo').raises(Backup::Errors::CLI::SystemCallError)
+ svn.local_repository_exists?("my_repo").should be_false
+ end
+ it "returns true when inside a working copy" do
+ svn.expects(:run).with('svnadmin verify backups/my_repo')
+ svn.local_repository_exists?("my_repo").should be_true
+ end
+ end
+
+ describe '#backup_repository!' do
+ context 'when the local repository exists' do
+ it 'invokes update_repository! only' do
+ svn.expects(:local_repository_exists?).with('/my/repo').returns(true)
+ svn.expects(:update_repository!).with('/my/repo')
+
+ svn.backup_repository!('/my/repo')
+ end
+ end
+ context 'when the local repository does not exist' do
+ it 'invokes initialize_repository! and then update_repository!' do
+
+ svn.expects(:local_repository_exists?).with('/my/repo').returns(false)
+ svn.expects(:initialize_repository!).with('/my/repo')
+ svn.expects(:update_repository!).with('/my/repo')
+
+ svn.backup_repository!('/my/repo')
+ end
+ end
+ end
+
+ describe '#initialize_repository!' do
+ it 'initializes an empty repository' do
+ absolute_path = '/home/jimmy/backups/my/repo'
+
+ Backup::Logger.expects(:message).with("Initializing empty svn repository in 'backups/my/repo'.")
+
+ svn.expects(:repository_absolute_local_path).with('/my/repo').returns(absolute_path)
+
+ svn.expects(:create_repository_local_container!).with('/my/repo')
+
+ svn.expects(:run).with("svnadmin create 'backups/my/repo'")
+ svn.expects(:run).with("echo '#!/bin/sh' > 'backups/my/repo/hooks/pre-revprop-change'")
+ svn.expects(:run).with("chmod +x 'backups/my/repo/hooks/pre-revprop-change'")
+ svn.expects(:run).with("svnsync init file://#{absolute_path} http://example.com/my/repo")
+
+ svn.initialize_repository!('/my/repo')
+ end
+ end
+
+ describe '#update_repository!' do
+ it 'updates an existing repository' do
+ absolute_path = '/home/jimmy/backups/my/repo'
+
+ Backup::Logger.expects(:message).with("Updating svn repository in 'backups/my/repo'.")
+
+ svn.expects(:repository_absolute_local_path).with('/my/repo').returns(absolute_path)
+ svn.expects(:run).with("svnsync sync file://#{absolute_path} --non-interactive")
+
+ svn.update_repository!('/my/repo')
+ end
+ end
+
+end
View
27 templates/cli/utility/syncer/scm_git
@@ -0,0 +1,27 @@
+ ##
+ # Git [Syncer]
+ #
+ # This mirrors a remote git repository
+ #
+ # Notes about usage:
+ #
+ # The backup must be performed on the server in which the final data is stored.
+ # In other words, this is not a PUSH-type backup, but a PULL one.
+ #
+ # Requirements:
+ #
+ # git must be accessible from the command line.
+ # The account performing the backup must be able to access the remote repository.
+
+ sync_with SCM::Git do |git|
+ git.protocol = "git" # Defaults to 'git'. Could also be "http" or "https"
+ git.port = 9418 # Optional. Could also be 80, 443, or a custom one
+ git.username = "peter" # optional
+ git.password = "secret" # optional
+ git.ip = "git.example.com"
+ git.path = "/home/backup/git" # Defaults to 'backups'. The path where the repos will be stored locally.
+ git.repositories do # absolute paths to repositories (git.example.com/project-1.git ...)
+ add '/project-1.git'
+ add '/another/project/repo.git'
+ end
+ end
View
31 templates/cli/utility/syncer/scm_svn
@@ -0,0 +1,31 @@
+ ##
+ # SVNSync [Syncer]
+ #
+ # This mirrors a remote svn repository using svnsync ( http://svn.apache.org/repos/asf/subversion/trunk/notes/svnsync.txt )
+ #
+ # The backup repositories are created using svnadmin.
+ #
+ # Notes about usage:
+ #
+ # As a result of how svsync works, the backup must be performed on the server in which the final backup is stored.
+ # In other words, this is not a PUSH-type backup, but a PULL one.
+ #
+ # Requirements:
+ #
+ # svn must be accessible from the command line.
+ # The account performing the backup must be able to access the remote svn repository.
+ # The account performing the backup must be able to perform svnadmin locally.
+ #
+
+ sync_with SCM::SVN do |svn|
+ svn.protocol = "http" # Defaults to 'http'. Could also be "http" or "svn"
+ svn.port = 80 # Optional. Could also be 443 (for https), 3690 (for svn) or a custom one
+ svn.username = "peter" # optional
+ svn.password = "secret" # optional
+ svn.ip = "svn.example.com"
+ svn.path = "/home/backup/svn" # Defaults to 'backups'. The path where the repos will be stored locally.
+ svn.repositories do # absolute paths to repositories (svn.example.com/project/trunk ...)
+ add '/project/trunk'
+ add '/another/project/repo'
+ end
+ end
Something went wrong with that request. Please try again.