/
darcs-to-git
executable file
·244 lines (205 loc) · 8.14 KB
/
darcs-to-git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env ruby
##
## Author: Steve Purcell, http://www.sanityinc.com/
## Obtain the latest version of this software here: http://git.sanityinc.com/
##
# TODO: import parallel darcs repos as git branches, identifying branch points
# TODO: recapture time zone: http://www.timeanddate.com/library/abbreviations/timezones/
# TODO: always use darcs branch
require 'ostruct'
require 'rexml/document'
# 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"
SRCREPO = ARGV[0]
if [nil, '--help', '-h'].include?(SRCREPO)
STDERR.write(<<-end_usage)
Creates git repositories from darcs repositories
usage: darcs-to-git DARCSREPODIR
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.
end_usage
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
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
self.author = patch_xml.attribute('author').value
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 = patch_xml.get_elements('name').first.get_text.value rescue 'Unnamed patch'
self.comment = 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, ''] # Could manufacture or insert email address here
end
@author_name = decode_darcs_escapes(@author_name)
# XXX: do the same for names/comments?
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)
# 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
inventory = File.open("#{GIT_PATCHES}", File::WRONLY | File::APPEND | File::CREAT)
inventory.puts "#{identifier} #{commit_id}"
inventory.close
end
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
output_nofail_of("grep", "#{identifier}", "#{GIT_PATCHES}").scan(/.* ([a-z0-9]+$)/).flatten.first
end
end
def darcs_date_to_git_date(utc,local)
# We ignore the timezone name (it may be ambiguous) and instead
# calculate the timezone offset ourselves
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)
# darcs example: Mon Oct 2 14:23:28 CEST 2006
# everything except timezone name is fixed-length
# if parsing failes we just use UTC
if local =~ /^(\w\w\w) (\w\w\w) ([ 1-9]\d) ([ 0-9]\d)\:(\d\d)\:(\d\d) \w* (\d\d\d\d)/ then
local_time = Time.utc($7,$2,$3,$4,$5,$6)
else
local_time = utc_time
end
offs = local_time - utc_time
t = local_time
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 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") }
end
patches = DarcsPatch.read_from_repo(SRCREPO)
patches_to_pull = []
while patch = patches.pop
break if patch.id_in_git_repo
patches_to_pull.unshift(patch)
end
patches_to_pull.each { |patch| patch.pull_and_apply }
puts "\ndarcs import successful. You may now want to run `git gc' to
improve space usage the git repo"