Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: d596b359a3
Fetching contributors…

Cannot retrieve contributors at this time

executable file 391 lines (341 sloc) 12.777 kb
#!/usr/bin/env ruby
##
## Author: Steve Purcell, http://www.sanityinc.com/
## Obtain the latest version of this software here: http://git.sanityinc.com/
##
# XXX: make backwards compatible
# TODO: import parallel darcs repos as git branches, identifying branch points
# TODO: use default repo if none was supplied
require 'ostruct'
require 'rexml/document'
require 'optparse'
require 'yaml'
# Explicitly setting a time zone would cause darcs to only output in
# that timezone hence we couldn't get the actual patch TZ
# ENV['TZ'] = 'GMT0'
# GIT_DARCS_BRANCH = "darcs_repo" # name of the branch we import to
GIT_PATCHES = ".git/darcs_patches"
AUTHOR_MAP_FILE = ".git/darcs_author_substitutions"
OPTIONS = { :default_email => nil,
:list_authors => false,
:author_map => nil,
:num_patches => 0 }
opts = OptionParser.new do |opts|
opts.banner = "Creates git repositories from darcs repositories
usage: darcs-to-git DARCSREPODIR [options]
1. Create an *empty* directory that will become the new git repository
2. From inside that directory, run this program, passing the location
of the local source darcs repo as a parameter
The program will git-init the empty directory, and migrate all patches
in the source darcs repo into commits in that repository.
Thereafter, incremental patch conversion from the same source repo is
possible by repeating step 2.
NOTE: In case of multiple tags, only the first one will be applied.
If you really need to, you can manually identify the patch and use
\"git tag -f <tagname> <sha1-of-commit-before-tagging>\".
OPTIONS
"
opts.on('--default-email ADDRESS',
"Set the email address used when no explicit address is given") do |m|
OPTIONS[:default_email] = m
end
opts.on('--list-authors',
"List all unique authors in source repo and quit.") do |m|
OPTIONS[:list_authors] = m
end
opts.on('--author-map FILE',
"Supply a YAML file that maps commiter names to canonical author names") do |f|
OPTIONS[:author_map] = f
end
opts.on('--patches [N]', OptionParser::DecimalInteger,
"Only pull N patches.") do |n|
OPTIONS[:num_patches] = n
end
end
opts.parse!
SRCREPO = ARGV[0]
if SRCREPO.nil? or not FileTest.exists?(SRCREPO + '/_darcs') then
if SRCREPO.nil? then
puts opts.banner()
puts opts.summarize()
else
puts "Argument must be a valid local darcs repository"
end
exit(1)
end
def run(*args)
puts "Running: #{args.inspect}"
system(*args) || raise("Failed to run: #{args.inspect}")
end
def output_of(*args)
puts "Running: #{args.inspect}"
output = IO.popen(args.map {|a| "'#{a}'"}.join(' '), 'r') { |p| p.read }
if $?.exitstatus == 0
return output
else
raise "Failed to run: #{args.inspect}"
end
end
# variant of output_of, but you have to check for success on your own
def output_nofail_of(*args)
puts "Running: #{args.inspect}"
output = IO.popen(args.map {|a| "'#{a}'"}.join(' '), 'r') { |p| p.read }
end
$darcs_patches_in_git = nil
class DarcsPatch
attr_accessor :source_repo, :author, :date, :inverted, :identifier, :name, :is_tag, :git_tag_name, :comment
attr_reader :author_name, :author_email
def initialize(source_repo, patch_xml)
self.source_repo = source_repo
darcs_author = decode_darcs_escapes(patch_xml.attribute('author').value)
self.author = AUTHOR_MAP.fetch(darcs_author, darcs_author)
self.date = darcs_date_to_git_date(patch_xml.attribute('date').value,
patch_xml.attribute('local_date').value)
self.inverted = (patch_xml.attribute('inverted').to_s == 'True')
self.identifier = patch_xml.attribute('hash').to_s
self.name = decode_darcs_escapes(patch_xml.get_elements('name').first.get_text.value) rescue 'Unnamed patch'
self.comment = decode_darcs_escapes(patch_xml.get_elements('comment').first.get_text.value) rescue nil
if (self.is_tag = (self.name =~ /^TAG (.*)/))
self.git_tag_name = $1.gsub(/[\s:]+/, '_')
end
author_scan
end
def <=>(other)
self.identifier <=> other.identifier
end
def git_commit_message
[ ((inverted ? "UNDO: #{name}" : name) unless name =~ /^\[\w+ @ \d+\]/),
comment
].compact.join("\n\n")
# "darcs-hash:#{identifier}" ].compact.join("\n\n")
end
def self.read_from_repo(repo)
REXML::Document.new(output_of("darcs", "changes", "--reverse",
"--repodir=#{repo}", "--xml",
"--summary")).
get_elements('changelog/patch').map do |p|
DarcsPatch.new(repo, p)
end
end
# Return committish for corresponding patch in current git repo, or false/nil
def id_in_git_repo
@git_commit ||= find_in_git_repo
end
def pull_and_apply
puts "\n" + ("=" * 80)
puts "PATCH : #{name}"
puts "DATE : #{date}"
puts "AUTHOR: #{author_name}"
puts "EMAIL : #{author_email}"
puts "=" * 80
if id_in_git_repo
puts "Already imported to git as #{id_in_git_repo}"
return
end
pull
system("git-status")
apply_to_git_repo
end
private
def author_scan
@author_name, @author_email =
if (author =~ /^\s*(\S.*?)\s*\<(\S+@\S+?)\>\s*$/)
[$1, $2]
elsif (author =~ /^\s*\<?(\S+@\S+?)\>?\s*$/)
email = $1
[email.split('@').first, email]
else
[author, OPTIONS[:default_email]]
end
end
def decode_darcs_escapes(str)
# darcs uses '[_\hh_]' to quote non-ascii characters where 'h' is
# a hexadecimal. We translate this to '=hh' and use ruby's unpack
# to do replace this with the proper byte.
str.gsub(/\[\_\\(..)\_\]/) { |x| "=#{$1}" }.unpack("M*")[0]
end
def pull
run("darcs", "pull", "--all", "--quiet",
"--match", "hash #{identifier}",
"--set-scripts-executable", source_repo)
unless `darcs whatsnew -sl` =~ /^No changes!$/
puts "Darcs reports dirty directory: assuming conflict that is fixed by a later patch... reverting"
run("darcs revert --all")
end
end
def apply_to_git_repo
ENV['GIT_AUTHOR_NAME'] = ENV['GIT_COMMITTER_NAME'] = author_name
ENV['GIT_AUTHOR_EMAIL'] = ENV['GIT_COMMITTER_EMAIL'] = author_email
ENV['GIT_AUTHOR_DATE'] = ENV['GIT_COMMITTER_DATE'] = date
if is_tag
run("git-tag", "-a", "-m", git_commit_message, git_tag_name)
else
if (new_files = git_new_files).any?
run(*(["git-add"] + new_files))
end
if git_changed_files.any? || new_files.any?
run("git-commit", "-a", "-m", git_commit_message)
end
# get full id of last commit and associate it with the patch id
commit_id = output_of("git-log", "-n1").scan(/^commit ([a-z0-9]+$)/).flatten.first
record_git_commit(commit_id)
end
end
def find_in_git_repo
return nil unless File.exists?(".git/refs/heads/master") # empty repo
if is_tag then
output_of("git-tag", "-l").split(/\r?\n/).include?(git_tag_name) &&
output_of("git-rev-list", "--max-count=1", "tags/#{git_tag_name}").strip
else
if $darcs_patches_in_git.nil? then
$darcs_patches_in_git = YAML.load_file("#{GIT_PATCHES}")
end
$darcs_patches_in_git[identifier];
end
end
def record_git_commit(commit_id)
# using one file per darcs patch would be an incredible waste of space
# on my system one file takes up 4K even if only a few bytes are in it
# hence we just use a simple YAML hash
unless $darcs_patches_in_git.nil?
$darcs_patches_in_git[identifier] = commit_id
end
File.open(GIT_PATCHES, File::WRONLY|File::APPEND|File::CREAT) do |f|
f.puts "#{identifier}: #{commit_id}"
end
end
def darcs_date_to_git_date(utc,local)
# Calculates a git-friendly date (e.g., timezone CET decribed as
# +0100) by using the two date fields that darcs gives us: a list
# of numbers describing the UTC time and a local time formatted in
# a human-readable format. We could parse the local time and
# derive the timezone offset from the timezone name. but timezones
# aren't well-defined, so we ignore the timezone name and instead
# calculate the timezone offset ourselves by calculating the
# difference between local time and ITC time.
if not utc =~ /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ then
raise "Wrong darcs date format"
end
utc_time = Time.utc($1,$2,$3,$4,$5,$6)
# example: Mon Oct 2 14:23:28 CEST 2006
# everything except timezone name is fixed-length, if parsing
# failes we just use UTC
pat = /^\w\w\w (\w\w\w) ([ 1-9]\d) ([ 0-9]\d)\:(\d\d)\:(\d\d) \w* (\d\d\d\d)/
if local =~ pat then
local_time = Time.utc($6,$1,$2,$3,$4,$5)
else
local_time = utc_time
end
offs = local_time - utc_time # time offset in seconds
t = local_time
# formats the above example as: 2006-10-02 14:23:28 +0200
s = sprintf("%4d-%02d-%02d %02d:%02d:%02d %s%02d%02d",
t.year, t.month, t.day,
t.hour, t.min, t.sec,
offs < 0 ? "-" : "+", offs.abs/3600, offs.abs.modulo(3600)/60 )
end
def git_ls_files(wanted)
output_of(*["git-ls-files", "-t", "-o", "-m", "-d", "-X", ".git/info/exclude"]).scan(/^(.?) (.*?)$/m).map do |code, name|
name if wanted.include?(code)
end.compact
end
def git_new_files() git_ls_files(["?"]) end
def git_changed_files() git_ls_files(%w(? R C)) end
end
def extract_authors(patches)
unique_authors = {}
patches.each do |p|
unique_authors[p.author] =
"#{p.author_name}" + (p.author_email.nil? ? "" : " <#{p.author_email}>")
end
puts "# You can use the following output as a starting point for an author_map"
puts "# Just fill in the proper text after the colon; put email addresses in"
puts "# angle brackets. You can remove any lines that look OK to you."
# TODO: Can we make the output sorted?
puts YAML::dump( unique_authors )
end
def darcs_version
output_of(*%w(darcs -v)).scan(/(\d+)\.(\d+)\.(\d+)/).flatten.map {|v| v.to_i}
end
class Array; include Comparable; end
unless darcs_version > [1, 0, 7]
STDERR.write("WARNING: your darcs appears to be old, and may not work with this script\n")
end
unless File.directory?("_darcs")
run("darcs", "init")
run("git-init")
run("touch", "#{GIT_PATCHES}")
File.open(".git/info/exclude", "a") { |f| f.write("_darcs\n.DS_Store\n") }
File.open("_darcs/prefs/boring", "a") { |f| f.write("\\.git$\n\\.DS_Store$\n") }
# TODO: migrate darcs borings into git excludes?
end
unless FileTest.exists?("#{GIT_PATCHES}")
# XXX: convert to new format
STDERR.puts "It seems your repo has been created with an old version of #{$0}."
exit(1)
end
unless OPTIONS[:default_email].nil?
run("git-config", "user.email", OPTIONS[:default_email])
end
if OPTIONS[:author_map] then
unless FileTest.exists?(OPTIONS[:author_map])
STDERR.puts "File #{OPTIONS[:author_map]} does not exist"
exit(1)
end
begin
AUTHOR_MAP = (YAML.load_file(OPTIONS[:author_map]) or {})
rescue
STDERR.puts "Could not parse #{(OPTIONS[:author_map])}"
exit(1)
end
unless AUTHOR_MAP.class == Hash
STDERR.puts "Wrong file format for author file."
exit(1)
end
File.open("#{AUTHOR_MAP_FILE}", File::WRONLY|File::CREAT) do |f|
YAML::dump(AUTHOR_MAP, f)
end
else
begin
AUTHOR_MAP = (YAML.load_file(AUTHOR_MAP_FILE) or {})
rescue
AUTHOR_MAP = {}
end
end
patches = DarcsPatch.read_from_repo(SRCREPO)
if OPTIONS[:list_authors] then
extract_authors(patches)
exit(0)
end
patches_to_pull = []
while patch = patches.pop
next if patch.id_in_git_repo
patches_to_pull.unshift(patch)
end
$run_consistency_check = true
if OPTIONS[:num_patches] > 0 then
# if we don't pull all patches, then the consistency check would
# fail, so we simply skip it
$run_consistency_check = patches_to_pull.length <= OPTIONS[:num_patches]
# only pull specified number of patches
patches_to_pull = patches_to_pull[0, OPTIONS[:num_patches]]
end
patches_to_pull.each { |patch| patch.pull_and_apply }
def check_consistent(pulled)
ok = true
if $run_consistency_check then
puts "Checking for consistency ..."
system("diff", "-ur", "-x", "_darcs", "-x", ".git", ".", SRCREPO)
ok = $? == 0
end
if ok then
puts "\nPulled #{pulled.length} patch#{pulled.length != 1 ? "es" : ""}."
puts "\nDarcs import successful! You may now want to run `git gc' to
improve space usage the git repo"
else
puts "!!! There were differences! See diff above for details."
puts "!!! It maybe that the source repository was dirty."
puts "!!! Run \"cd #{SRCREPO} && darcs whatsnew -sl\" to check."
end
end
check_consistent(patches_to_pull)
Jump to Line
Something went wrong with that request. Please try again.