Commits

Seth Jackson  committed d9577cb

Fix updating private repos. Also use the JSON gem which is avaliable for ruby 1.8.x and comes with 1.9.x by default. Wiki updates are broken again because not enough information is being returned from the BB API.

  • Participants
  • Tags v0.2.1

Comments (0)

Files changed (11)

+syntax: glob
+
+Gemfile.lock
+*.gem
+
+.yardoc
+doc/
+pkg/
+source "https://rubygems.org"
+
+gemspec
+Copyright (c) 2012, Seth Jackson
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+# Bitbucket Backup
+
+A command line tool for backing up Bitbucket repos.
+
+
+## Prerequisites
+
+* [Mercurial](http://mercurial.selenic.com/)
+* [Git](http://git-scm.com/)
+* [Ruby](http://www.ruby-lang.org/)
+
+To backup your repos you **must** have the Mercurial and/or the Git binaries on your path.
+
+
+## Installation
+
+    $ gem install bitbucket-backup
+
+
+## Basic Usage
+
+    $ bitbucket-backup {path-to-backup-to}
+
+
+## License
+
+This backup tool is licensed under an ISC-style license.
+See the file 'LICENSE' for more details.
+#!/usr/bin/env rake
+
+require "bundler/gem_tasks"

File bin/bitbucket-backup

+#!/usr/bin/env ruby
+
+require "highline/import"
+require "bitbucket-backup"
+
+if ARGV.empty?
+    puts "Must specify a path to backup repos to."
+    
+    exit
+end
+
+username = ask("Username: ")
+
+password = ask("Password: ") do |q|
+    q.echo = false
+end
+
+Bitbucket::Backup.run(username, password, ARGV.first)

File bitbucket-backup.gemspec

+$:.push File.expand_path("../lib", __FILE__)
+
+require "bitbucket-backup/version"
+
+Gem::Specification.new do |s|
+    s.name        = "bitbucket-backup"
+    s.version     = Bitbucket::Backup::VERSION
+    s.authors     = ["Seth Jackson"]
+    s.email       = "sethjackson@gmail.com"
+    s.homepage    = "https://bitbucket.org/seth/bitbucket-backup"
+    s.summary     = "A tool to backup Bitbucket repos."
+    s.description = "A tool to backup Bitbucket repos."
+    
+    s.executables = "bitbucket-backup"
+    
+    s.files = Dir["lib/**/*"] + %w{bitbucket-backup.gemspec Gemfile LICENSE Rakefile README.md}
+    
+    s.add_dependency "highline"
+    s.add_dependency "json"
+end

File lib/bitbucket-backup.rb

+require "bitbucket-backup/backup"
+require "bitbucket-backup/repository"
+require "bitbucket-backup/version"

File lib/bitbucket-backup/backup.rb

+require "net/https"
+require "json"
+
+module Bitbucket
+    module Backup
+        # Begins the backup process.
+        #
+        # @param [String] username
+        #     the Bitbucket username of the user to backup repos for.
+        #
+        # @param [String] password
+        #     the plain-text password of the user to backup repos for.
+        #
+        # @param [String] backup_root
+        #     the absolute or relative path of the directory to backup the repos to.
+        #
+        def self.run(username, password, backup_root)
+            backup_root = File.expand_path(backup_root)
+            
+            puts
+            puts "Backing up repos to #{backup_root}"
+            puts
+            
+            repos = get_repo_list(username, password)
+            
+            repos.each do |repo|
+                # Only backup repos that the user owns.
+                if repo["owner"] == username
+                    Bitbucket::Backup::Repository.new(repo, password, backup_root).backup
+                end
+            end
+        end
+        
+        def self.have_scm_tool?(cmd)
+            # From: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
+            exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
+            
+            ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
+                exts.each do |ext|
+                    bin = "#{path}/#{cmd}#{ext}"
+                    
+                    return bin if File.executable?(bin)
+                end
+            end
+            
+            return nil
+        end
+        
+        private
+        # Gets the user's repos.
+        #
+        # @return [Array<String>]
+        #     the repos.
+        #
+        def self.get_repo_list(username, password)
+            uri = URI.parse("https://api.bitbucket.org/1.0/user/repositories/")
+            
+            http = Net::HTTP.new(uri.host, uri.port)
+            
+            http.use_ssl = true
+            http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+            
+            request = Net::HTTP::Get.new(uri.request_uri)
+            request.basic_auth(username, password)
+            
+            response = http.request(request)
+            
+            # authentication didn't work
+            if response.code == "401"
+                puts "Invalid username/password."
+                
+                exit
+            end
+            
+            JSON.parse(response.body)
+        end
+    end
+end

File lib/bitbucket-backup/repository.rb

+require "fileutils"
+require "cgi"
+require "open3"
+
+module Bitbucket
+    module Backup
+        class Repository
+            # @return [Hash]
+            #     the hash of repo data from the Bitbucket API.
+            #
+            attr_reader :repo
+            
+            # @return [String]
+            #     the plain-text password for the repo.
+            #
+            attr_reader :password
+            
+            # @return [String]
+            #     the absolute path of the directory to backup the repo to.
+            #
+            attr_reader :backup_root
+            
+            # Creates a new repo.
+            #
+            # @param [Hash] repo
+            #     the hash of repo data from the Bitbucket API.
+            #
+            # @param [String] password
+            #     the plain-text passsword for the repo.
+            #
+            # @param [String] backup_root
+            #     the absolute path of the directory to backup the repo to.
+            #
+            def initialize(repo, password, backup_root)
+                @repo        = repo
+                @password    = password
+                @backup_root = backup_root
+            end
+            
+            # Performs a backup of the repository.
+            #
+            def backup
+                puts "Backing up: #{repo["slug"]}."
+                
+                unless Bitbucket::Backup.have_scm_tool?(repo["scm"])
+                    puts "Warning: #{repo["scm"]} not found on PATH. Skipping..."
+                    
+                    return
+                end
+                
+                backup_src
+                
+                backup_wiki
+            end
+            
+            private
+            def backup_src
+                clone_or_update(:type => :src)
+            end
+            
+            def backup_wiki
+                clone_or_update(:type => :wiki) if repo["has_wiki"]
+            end
+            
+            def clone_or_update(options)
+                path = dir_for_repo(options)
+                uri  = uri_for_repo(options)
+                
+                if File.exist?(path)
+                   run_incremental_backup(path, uri)
+                else
+                   run_full_backup(uri, path)
+                end
+            end
+            
+            def is_repo?(path)
+                FileUtils.cd(path)
+                
+                case repo["scm"]
+                    when "hg"
+                        Open3.popen3("hg status")
+                    when "git"
+                        Open3.popen3("git status")
+                end
+                
+                return $.
+            end
+            
+            def run_incremental_backup(path, uri)
+                unless is_repo?(path)
+                    puts "Warning: file already exists but is not a repo!! Skipping..."
+                    
+                    return false
+                end
+                
+                FileUtils.cd(path)
+                
+                case repo["scm"]
+                    when "hg"
+                        system "hg pull -u #{uri}"
+                    when "git"
+                        system "git pull #{uri}"
+                end
+            end
+            
+            def run_full_backup(uri, dest)
+                case repo["scm"]
+                    when "hg"
+                        system "hg clone #{uri} #{dest}"
+                    when "git"
+                        system "git clone #{uri} #{dest}"
+                end
+            end
+            
+            def dir_for_repo(options)
+                if options.nil? || options[:type].nil?
+                    raise RuntimeError
+                end
+                
+                path = nil
+                
+                case options[:type]
+                    when :src
+                        path = File.expand_path("#{backup_root}/#{repo["slug"]}/src")
+                    when :wiki
+                        path = File.expand_path("#{backup_root}/#{repo["slug"]}/wiki")
+                end
+                
+                return path
+            end
+            
+            def uri_for_repo(options)
+                base_uri = nil
+                uri      = nil
+                ext      = nil
+                
+                if repo["scm"] == "git"
+                    ext = ".git"
+                end
+                
+                if repo["is_private"]
+                    base_uri = "https://#{repo["owner"]}:#{CGI.escape(password)}@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}#{ext}"
+                else
+                    base_uri = "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}#{ext}"
+                end
+                
+                case options[:type]
+                    when :src
+                        uri = base_uri
+                    when :wiki
+                        uri = "#{base_uri}/wiki"
+                end
+                
+                return uri
+            end
+        end
+    end
+end

File lib/bitbucket-backup/version.rb

+module Bitbucket
+    module Backup
+        VERSION = "0.2.1"
+    end
+end