4
4
# The git history.
5
5
module CodeExtractor
6
6
def run
7
- Runner . new . extract
7
+ Runner . new . run
8
8
end
9
9
module_function :run
10
10
@@ -34,7 +34,7 @@ def validate!
34
34
end
35
35
36
36
class GitProject
37
- attr_reader :name , :url , :git_dir , :new_branch , :source_branch
37
+ attr_reader :name , :url , :git_dir , :new_branch , :source_branch , :target_name
38
38
39
39
def initialize name , url
40
40
@name = name
@@ -58,7 +58,7 @@ def extract_branch source_branch, new_branch, extractions
58
58
@source_branch = source_branch
59
59
Dir . chdir git_dir do
60
60
`git checkout #{ source_branch } `
61
- `git fetch upstream && git rebase upstream/master `
61
+ `git fetch upstream && git rebase upstream/#{ source_branch } `
62
62
if system ( "git branch | grep #{ new_branch } " )
63
63
`git branch -D #{ new_branch } `
64
64
end
@@ -85,18 +85,205 @@ def remove_tags
85
85
end
86
86
end
87
87
88
- def filter_branch extractions , upstream_name
88
+ def extract_commits extractions , upstream_name
89
89
Dir . chdir git_dir do
90
90
`time git filter-branch --index-filter '
91
91
git read-tree --empty
92
92
git reset $GIT_COMMIT -- #{ extractions }
93
+ ' #{ msg_filter upstream_name } -- #{ source_branch } -- #{ extractions } `
94
+ end
95
+ end
96
+
97
+ def prune_commits extractions
98
+ puts "Pruning commits…"
99
+
100
+ build_prune_script extractions
101
+
102
+ Dir . chdir git_dir do
103
+ `git checkout -b #{ prune_branch } #{ @source_branch } `
104
+ `git filter-branch -f --prune-empty --tree-filter #{ @prune_script } HEAD`
105
+ `git filter-branch -f --prune-empty --subdirectory-filter #{ @keep_directory } `
106
+ end
107
+ end
108
+
109
+ def add_target_remote target_name , target_remote
110
+ puts "Add target repo as a remote…"
111
+ @target_name = target_name
112
+
113
+ Dir . chdir git_dir do
114
+ puts "git remote add #{ target_remote_name } #{ target_remote } "
115
+ `git remote add #{ target_remote_name } #{ target_remote } `
116
+ `git fetch #{ target_remote_name } `
117
+ end
118
+ end
119
+
120
+ # "Inject" commits one repo's branch back into the target repo's
121
+ #
122
+ # Assuming the target remote has been added (see add_target_remote), this
123
+ # method does so by doing the following to achieve the "injected" history:
124
+ #
125
+ # 1. Filters commits that already exist in the target repo. Additionally,
126
+ # the last commit that is shared between the two is actually used as the
127
+ # "root" commit for the injected commits. The rest are assumed to be new
128
+ # from the new repository.
129
+ #
130
+ # The "root" commit has it's commit message modified to reflect this
131
+ # change.
132
+ #
133
+ # 2. A new branch is checked out that is based off the target remote's
134
+ # target branch, but does not track that branch.
135
+ #
136
+ # 3. The commits that have been filtered are cherry-picked on to this new
137
+ # branch, and the "root" commit assumes the parent of the current HEAD of
138
+ # the target remote's (master) branch
139
+ #
140
+ def inject_commits target_base_branch , upstream_name
141
+ puts "Injecting commits…"
142
+
143
+ target_base_branch ||= 'master'
144
+ commit_msg_filter = "(transferred from #{ upstream_name } "
145
+
146
+ Dir . chdir git_dir do
147
+ reference_target_branch = "#{ target_remote_name } /#{ target_base_branch } "
148
+ previously_extracted_commits = `git log --pretty="%H" --grep="#{ commit_msg_filter } "`
149
+
150
+ # special commit that will get renamed re-worded to:
151
+ #
152
+ # Re-insert extractions from #{upstream_name}
153
+ #
154
+ last_extracted_commit = previously_extracted_commits . lines [ 0 ] . chomp!
155
+ first_injected_msg = `git show -s --format="%s%n%n%b" #{ last_extracted_commit } `
156
+ first_injected_msg = first_injected_msg . lines . reject { |line |
157
+ line . include? commit_msg_filter
158
+ } . join
159
+ first_injected_msg . prepend "*** Original Commit message shown below ***\n \n "
160
+ first_injected_msg . prepend "Re-insert extractions from #{ target_name } \n \n "
161
+ File . write File . expand_path ( "../LAST_EXTRACTED_COMMIT_MSG" , git_dir ) , first_injected_msg
162
+
163
+ `time git filter-branch -f --commit-filter '
164
+ export was_extracted=$(git show -s --format="%s%n%n%b" $GIT_COMMIT | grep -s "#{ commit_msg_filter } ")
165
+ if [ "$GIT_COMMIT" = "#{ last_extracted_commit } " ] || [ "$was_extracted" == "" ]; then
166
+ git commit-tree "$@";
167
+ else
168
+ skip_commit "$@";
169
+ fi
170
+ ' --index-filter '
171
+ git read-tree --empty
172
+ git reset #{ reference_target_branch } -- .
173
+ git checkout $GIT_COMMIT -- .
93
174
' --msg-filter '
175
+ if [ "$GIT_COMMIT" = "#{ last_extracted_commit } " ]; then
176
+ cat #{ File . expand_path File . join ( ".." , "LAST_EXTRACTED_COMMIT_MSG" ) , git_dir }
177
+ else
178
+ cat -
179
+ fi
180
+ echo
181
+ echo
182
+ echo "(transferred from #{ upstream_name } @$GIT_COMMIT)"
183
+ ' -- #{ prune_branch } `
184
+
185
+ `git checkout --no-track -b #{ inject_branch } #{ reference_target_branch } `
186
+ `git cherry-pick ..#{ prune_branch } `
187
+ end
188
+ end
189
+
190
+ def run_extra_cmds cmds
191
+ Dir . chdir git_dir do
192
+ cmds . each { |cmd | system cmd } if cmds
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def target_remote_name
199
+ @target_remote_name ||= "code_extractor_target_for_#{ name } "
200
+ end
201
+
202
+ def prune_branch
203
+ @prune_branch ||= "code_extractor_prune_#{ name } "
204
+ end
205
+ alias prune_commits_remote prune_branch
206
+
207
+ def inject_branch
208
+ @inject_branch ||= "code_extractor_inject_#{ name } "
209
+ end
210
+ alias inject_remote inject_branch
211
+
212
+ # Given a list of extractions, build a script that will move a list of
213
+ # files (extractions) from their current location in a given commit to a
214
+ # unused directory.
215
+ #
216
+ # More complicated than it looks, this will be used as part of a two-phased
217
+ # `git filter-branch` to:
218
+ #
219
+ # 1. move extractable files into a subdirectory with `--tree-filter`
220
+ # 2. only keep commits for files moved into that subdirectory, and make
221
+ # the subdirectory the new project root.
222
+ #
223
+ # For consistency, we want to keep the subdirectories' structure in the
224
+ # same line as what was there previously, so this script helps do that, and
225
+ # also creates directories/files when they don't exist.
226
+ #
227
+ # Returns `true` at the end of the script incase the last `mv` fails (the
228
+ # source doesn't exist in this commit, for example)
229
+ #
230
+ def build_prune_script extractions
231
+ require 'fileutils'
232
+
233
+ @keep_directory = "code_extractor_git_keeps_#{ Time . now . to_i } "
234
+ git_log_follow = "git log --name-only --format=format: --follow"
235
+ prune_mkdirs = Set . new
236
+ prune_mvs = [ ]
237
+
238
+ Dir . chdir git_dir do
239
+ extractions . each do |file_or_dir |
240
+ if Dir . exist? file_or_dir
241
+ files = Dir . glob [ "#{ file_or_dir } /**/*" ]
242
+ else
243
+ files = [ file_or_dir ]
244
+ end
245
+
246
+ files . each do |extraction_file |
247
+ file_and_ancestors = `#{ git_log_follow } -- #{ extraction_file } ` . split ( "\n " ) . uniq
248
+
249
+ file_and_ancestors . reject! { |file | file . length == 0 }
250
+
251
+ file_and_ancestors . each do |file |
252
+ file_dir = File . dirname file
253
+ prune_mkdirs . add file_dir
254
+ prune_mvs << [ file , "#{ @keep_directory } /#{ file_dir } " ]
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ @prune_script = File . join Dir . pwd , "code_extractor_#{ name } _prune_script.sh"
261
+
262
+ File . open @prune_script , "w" do |script |
263
+ prune_mkdirs . each do |dir |
264
+ script . puts "mkdir -p #{ File . join @keep_directory , dir } "
265
+ end
266
+
267
+ script . puts
268
+ prune_mvs . each do |( file , dir ) |
269
+ script . puts "mv #{ file } #{ dir } 2>/dev/null"
270
+ end
271
+
272
+ script . puts
273
+ script . puts "true"
274
+ end
275
+ FileUtils . chmod "+x" , @prune_script
276
+ end
277
+
278
+ def msg_filter upstream_name
279
+ <<-MSG_FILTER . gsub ( /^ {8}/ , '' ) . chomp
280
+ --msg-filter '
94
281
cat -
95
282
echo
96
283
echo
97
284
echo "(transferred from #{ upstream_name } @$GIT_COMMIT)"
98
- ' -- #{ source_branch } -- #{ extractions } `
99
- end
285
+ '
286
+ MSG_FILTER
100
287
end
101
288
end
102
289
@@ -106,17 +293,39 @@ def initialize config = nil
106
293
@source_project = GitProject . new @config [ :name ] , @config [ :upstream ]
107
294
end
108
295
109
- def extractions
110
- @extractions ||= @config [ :extractions ] . join ( ' ' )
111
- end
112
-
113
- def extract
296
+ # Either run `.reinsert` or `.extract`
297
+ #
298
+ # The `.reinsert` method will eject with `nil` unless the config setting to
299
+ # run in that mode is set
300
+ #
301
+ def run
114
302
puts @config
303
+
115
304
@source_project . clone_to @config [ :destination ]
116
305
@source_project . extract_branch @config [ :upstream_branch ] , "extract_#{ @config [ :name ] } " , extractions
117
306
@source_project . remove_remote
118
307
@source_project . remove_tags
119
- @source_project . filter_branch extractions , @config [ :upstream_name ]
308
+
309
+ reinsert || extract
310
+ end
311
+
312
+ def extractions
313
+ @extractions ||= @config [ :extractions ] . join ( ' ' )
314
+ end
315
+
316
+ def extract
317
+ @source_project . extract_commits extractions , @config [ :upstream_name ]
318
+ end
319
+
320
+ def reinsert
321
+ return unless @config [ :reinsert ]
322
+
323
+ @source_project . prune_commits @config [ :extractions ]
324
+ @source_project . run_extra_cmds @config [ :extra_cmds ]
325
+ @source_project . add_target_remote @config [ :target_name ] , @config [ :target_remote ]
326
+ @source_project . inject_commits @config [ :target_base_branch ] , @config [ :upstream_name ]
327
+
328
+ true
120
329
end
121
330
end
122
331
end
0 commit comments