di 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README.rdoc +12 -9
  2. data/Rakefile +1 -1
  3. data/bin/di +6 -818
  4. data/di.gemspec +5 -4
  5. data/lib/di.rb +822 -0
  6. metadata +12 -4
data/di.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{di}
8
- s.version = "0.1.8"
8
+ s.version = "0.1.9"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Akinori MUSHA"]
12
- s.date = %q{2010-03-11}
12
+ s.date = %q{2010-05-06}
13
13
  s.default_executable = %q{di}
14
14
  s.description = %q{The di(1) command wraps around GNU diff(1) to provide reasonable
15
15
  default settings and some original features.
@@ -29,6 +29,7 @@ default settings and some original features.
29
29
  "Rakefile",
30
30
  "bin/di",
31
31
  "di.gemspec",
32
+ "lib/di.rb",
32
33
  "test/helper.rb",
33
34
  "test/test_di.rb"
34
35
  ]
@@ -36,7 +37,7 @@ default settings and some original features.
36
37
  s.rdoc_options = ["--charset=UTF-8"]
37
38
  s.require_paths = ["lib"]
38
39
  s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
39
- s.rubygems_version = %q{1.3.6}
40
+ s.rubygems_version = %q{1.3.7.pre.1}
40
41
  s.summary = %q{A wrapper around GNU diff(1)}
41
42
  s.test_files = [
42
43
  "test/helper.rb",
@@ -47,7 +48,7 @@ default settings and some original features.
47
48
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
49
  s.specification_version = 3
49
50
 
50
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
52
  s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
52
53
  else
53
54
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
data/lib/di.rb ADDED
@@ -0,0 +1,822 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- mode: ruby; coding: utf-8 -*-
3
+ #
4
+ # di - a wrapper around GNU diff(1)
5
+ #
6
+ # Copyright (c) 2008, 2009, 2010 Akinori MUSHA
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions
12
+ # are met:
13
+ # 1. Redistributions of source code must retain the above copyright
14
+ # notice, this list of conditions and the following disclaimer.
15
+ # 2. Redistributions in binary form must reproduce the above copyright
16
+ # notice, this list of conditions and the following disclaimer in the
17
+ # documentation and/or other materials provided with the distribution.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25
+ # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29
+ # SUCH DAMAGE.
30
+
31
+ MYVERSION = "0.1.9"
32
+ MYNAME = File.basename($0)
33
+ MYCOPYRIGHT = "Copyright (c) 2008, 2009, 2010 Akinori MUSHA"
34
+
35
+ DIFF_CMD = ENV.fetch('DIFF', 'diff')
36
+ ENV_NAME = "#{MYNAME.tr('-a-z', '_A-Z')}_OPTIONS"
37
+ EMPTYFILE = '/dev/null'
38
+
39
+ RSYNC_EXCLUDE_GLOBS = %w(
40
+ RCS SCCS CVS CVS.adm
41
+ RCSLOG cvslog.* tags TAGS
42
+ .make.state .nse_depinfo *~
43
+ \#* .\#* ,* _$*
44
+ *$ *.old *.bak *.BAK
45
+ *.orig *.rej *.del-* *.a
46
+ *.olb *.o *.obj *.so
47
+ *.exe *.Z *.elc *.ln
48
+ core .svn .git .bzr .hg
49
+ )
50
+
51
+ FIGNORE_GLOBS = ENV.fetch('FIGNORE', '').split(':').map { |pat|
52
+ '*' + pat
53
+ }
54
+
55
+ def main(args)
56
+ setup
57
+
58
+ parse_args!(args)
59
+
60
+ diff_main
61
+
62
+ exit $status
63
+ end
64
+
65
+ def warn(*lines)
66
+ lines.each { |line|
67
+ STDERR.puts "#{MYNAME}: #{line}"
68
+ }
69
+ end
70
+
71
+ def setup
72
+ require 'ostruct'
73
+ $diff = OpenStruct.new
74
+ $diff.exclude = []
75
+ $diff.include = []
76
+ $diff.flags = []
77
+ $diff.format_flags = []
78
+ $diff.format = :normal
79
+ $diff.custom_format_p = false
80
+ $diff.use_pager = false
81
+ $diff.colorize = false
82
+ $diff.highlight_whitespace = true
83
+ $diff.colors = {
84
+ :comment => "\033[1m",
85
+ :file1 => "\033[1m",
86
+ :file2 => "\033[1m",
87
+ :header => "\033[36m",
88
+ :function => "\033[m",
89
+ :new => "\033[32m",
90
+ :old => "\033[31m",
91
+ :changed => "\033[33m",
92
+ :unchanged => "",
93
+ :whitespace => "\033[41m",
94
+ :off => "\033[m",
95
+ }
96
+ end
97
+
98
+ def parse_args!(args)
99
+ require 'optparse'
100
+
101
+ banner = <<-"EOF"
102
+ #{MYNAME} - a wrapper around GNU diff(1)
103
+ version #{MYVERSION}
104
+
105
+ usage: #{MYNAME} [flags] [files]
106
+ EOF
107
+
108
+ opts = OptionParser.new(banner) { |opts|
109
+ miniTrueClass = Class.new
110
+ hash = OptionParser::CompletingHash.new
111
+ hash['-'] = false
112
+ opts.accept(miniTrueClass, hash) {|arg, val| val == nil or val}
113
+
114
+ opts.on('--[no-]pager',
115
+ 'Pipe output into pager if stdout is a terminal. [!][*]') { |val|
116
+ $diff.use_pager = val if $stdout.tty?
117
+ }
118
+ opts.on('--[no-]color',
119
+ 'Colorize output if stdout is a terminal and the format is unified or context. [!][*]') { |val|
120
+ $diff.colorize = val if $stdout.tty?
121
+ }
122
+ opts.on('--[no-]rsync-exclude', '--[no-]cvs-exclude',
123
+ 'Exclude some kinds of files and directories a la rsync(1). [!][*]') { |val|
124
+ $diff.rsync_exclude = val
125
+ }
126
+ opts.on('--[no-]ignore-cvs-lines',
127
+ 'Ignore CVS keyword lines. [!][*]') { |val|
128
+ $diff.ignore_cvs_lines = val
129
+ }
130
+ opts.on('--[no-]fignore-exclude',
131
+ 'Ignore files having suffixes specified in FIGNORE. [!][*]') { |val|
132
+ $diff.fignore_exclude = val
133
+ }
134
+ opts.on('-R', '--relative[=-]', miniTrueClass,
135
+ 'Use relative path names. [*]') { |val|
136
+ $diff.relative = val
137
+ }
138
+ opts.on('-i', '--ignore-case[=-]', miniTrueClass,
139
+ 'Ignore case differences in file contents.') { |val|
140
+ set_flag('-i', val)
141
+ }
142
+ # not supported (yet)
143
+ #opts.on("--[no-]ignore-file-name-case",
144
+ # "Ignore case when comparing file names.") { |val|
145
+ # set_flag("--ignore-file-name-case", val)
146
+ #}
147
+ opts.on('-E', '--ignore-tab-expansion[=-]', miniTrueClass,
148
+ 'Ignore changes due to tab expansion.') { |val|
149
+ set_flag('-E', val)
150
+ }
151
+ opts.on('-b', '--ignore-space-change[=-]', miniTrueClass,
152
+ 'Ignore changes in the amount of white space.') { |val|
153
+ set_flag('-b', val)
154
+ }
155
+ opts.on('-w', '--ignore-all-space[=-]', miniTrueClass,
156
+ 'Ignore all white space.') { |val|
157
+ set_flag('-w', val)
158
+ }
159
+ opts.on('-B', '--ignore-blank-lines[=-]', miniTrueClass,
160
+ 'Ignore changes whose lines are all blank.') { |val|
161
+ set_flag('-B', val)
162
+ }
163
+ opts.on('-I RE', '--ignore-matching-lines=RE',
164
+ 'Ignore changes whose lines all match RE.') { |val|
165
+ set_flag('-I', val)
166
+ }
167
+ opts.on('--[no-]strip-trailing-cr',
168
+ 'Strip trailing carriage return on input.') { |val|
169
+ set_flag('--strip-trailing-cr', val)
170
+ }
171
+ opts.on('-a', '--text[=-]', miniTrueClass,
172
+ 'Treat all files as text.') { |val|
173
+ set_flag('-a', val)
174
+ }
175
+ opts.on('-c[NUM]', '--context[=NUM]', Integer,
176
+ 'Output NUM (default 3) lines of copied context.') { |val|
177
+ set_format_flag('-C', val ? val.to_s : '3')
178
+ }
179
+ opts.on('-C NUM', Integer,
180
+ 'Output NUM lines of copied context.') { |val|
181
+ set_format_flag('-C', val.to_s)
182
+ }
183
+ opts.on('-u[NUM]', '--unified[=NUM]', Integer,
184
+ 'Output NUM (default 3) lines of unified context. [!]') { |val|
185
+ set_format_flag('-U', val ? val.to_s : '3')
186
+ }
187
+ opts.on('-U NUM', Integer,
188
+ 'Output NUM lines of unified context.') { |val|
189
+ set_format_flag('-U', val.to_s)
190
+ }
191
+ opts.on('-L LABEL', '--label=LABEL',
192
+ 'Use LABEL instead of file name.') { |val|
193
+ set_flag('-L', val)
194
+ }
195
+ opts.on('-p', '--show-c-function[=-]', miniTrueClass,
196
+ 'Show which C function each change is in. [!]') { |val|
197
+ set_flag('-p', val)
198
+ }
199
+ opts.on('-F RE', '--show-function-line=RE',
200
+ 'Show the most recent line matching RE.') { |val|
201
+ set_flag('-F', val)
202
+ }
203
+ opts.on('-q', '--brief[=-]', miniTrueClass,
204
+ 'Output only whether files differ.') { |val|
205
+ set_flag('-q', val)
206
+ }
207
+ opts.on('-e', '--ed[=-]', miniTrueClass,
208
+ 'Output an ed script.') { |val|
209
+ if val
210
+ set_format_flag('-e', val)
211
+ end
212
+ }
213
+ opts.on('--normal[=-]', miniTrueClass,
214
+ 'Output a normal diff.') { |val|
215
+ if val
216
+ set_format_flag('--normal', val)
217
+ end
218
+ }
219
+ opts.on('-n', '--rcs[=-]', miniTrueClass,
220
+ 'Output an RCS format diff.') { |val|
221
+ if val
222
+ set_format_flag('-n', val)
223
+ end
224
+ }
225
+ opts.on('-y', '--side-by-side[=-]', miniTrueClass,
226
+ 'Output in two columns.') { |val|
227
+ if val
228
+ set_format_flag('-y', val)
229
+ end
230
+ }
231
+ opts.on('-W NUM', '--width=NUM', Integer,
232
+ 'Output at most NUM (default 130) print columns.') { |val|
233
+ set_flag('-W', val.to_s)
234
+ }
235
+ opts.on('--left-column[=-]', miniTrueClass,
236
+ 'Output only the left column of common lines.') { |val|
237
+ set_flag('--left-column', val)
238
+ }
239
+ opts.on('--suppress-common-lines[=-]', miniTrueClass,
240
+ 'Do not output common lines.') { |val|
241
+ set_flag('--suppress-common-lines', val)
242
+ }
243
+ opts.on('-D NAME', '--ifdef=NAME',
244
+ 'Output merged file to show `#ifdef NAME\' diffs.') { |val|
245
+ set_format_flag('-D', val)
246
+ }
247
+ opts.on('--old-group-format=GFMT',
248
+ 'Format old input groups with GFMT.') { |val|
249
+ set_custom_format_flag('--old-group-format', val)
250
+ }
251
+ opts.on('--new-group-format=GFMT',
252
+ 'Format new input groups with GFMT.') { |val|
253
+ set_custom_format_flag('--new-group-format', val)
254
+ }
255
+ opts.on('--changed-group-format=GFMT',
256
+ 'Format changed input groups with GFMT.') { |val|
257
+ set_custom_format_flag('--changed-group-format', val)
258
+ }
259
+ opts.on('--unchanged-group-format=GFMT',
260
+ 'Format unchanged input groups with GFMT.') { |val|
261
+ set_custom_format_flag('--unchanged-group-format', val)
262
+ }
263
+ opts.on('--line-format=LFMT',
264
+ 'Format all input lines with LFMT.') { |val|
265
+ set_custom_format_flag('--line-format', val)
266
+ }
267
+ opts.on('--old-line-format=LFMT',
268
+ 'Format old input lines with LFMT.') { |val|
269
+ set_custom_format_flag('--old-line-format', val)
270
+ }
271
+ opts.on('--new-line-format=LFMT',
272
+ 'Format new input lines with LFMT.') { |val|
273
+ set_custom_format_flag('--new-line-format', val)
274
+ }
275
+ opts.on('--unchanged-line-format=LFMT',
276
+ 'Format unchanged input lines with LFMT.') { |val|
277
+ set_custom_format_flag('--unchanged-line-format', val)
278
+ }
279
+ opts.on('-l', '--paginate[=-]', miniTrueClass,
280
+ 'Pass the output through `pr\' to paginate it.') { |val|
281
+ set_flag('-l', val)
282
+ }
283
+ opts.on('-t', '--expand-tabs[=-]', miniTrueClass,
284
+ 'Expand tabs to spaces in output.') { |val|
285
+ set_flag('-t', val)
286
+ }
287
+ opts.on('-T', '--initial-tab[=-]', miniTrueClass,
288
+ 'Make tabs line up by prepending a tab.') { |val|
289
+ set_flag('-T', '--initial-tab', val)
290
+ }
291
+ opts.on('--tabsize=NUM', Integer,
292
+ 'Tab stops are every NUM (default 8) print columns.') { |val|
293
+ set_flag('--tabsize', val.to_s)
294
+ }
295
+ opts.on('-r', '--recursive[=-]', miniTrueClass,
296
+ 'Recursively compare any subdirectories found. [!]') { |val|
297
+ set_flag('-r', val)
298
+ $diff.recursive = val
299
+ }
300
+ opts.on('-N', '--[no-]new-file[=-]', miniTrueClass,
301
+ 'Treat absent files as empty. [!]') { |val|
302
+ set_flag('-N', val)
303
+ $diff.new_file = val
304
+ }
305
+ opts.on('--unidirectional-new-file[=-]', miniTrueClass,
306
+ 'Treat absent first files as empty.') { |val|
307
+ set_flag('--unidirectional-new-file', val)
308
+ }
309
+ opts.on('-s', '--report-identical-files[=-]', miniTrueClass,
310
+ 'Report when two files are the same.') { |val|
311
+ set_flag('-s', val)
312
+ }
313
+ opts.on('-x PAT', '--exclude=PAT',
314
+ 'Exclude files that match PAT.') { |val|
315
+ $diff.exclude << val
316
+ }
317
+ opts.on('-X FILE', '--exclude-from=FILE',
318
+ 'Exclude files that match any pattern in FILE.') { |val|
319
+ if val == '-'
320
+ $diff.exclude.concat(STDIN.read.split(/\n/))
321
+ else
322
+ $diff.exclude.concat(File.read(val).split(/\n/))
323
+ end
324
+ }
325
+ opts.on('--include=PAT',
326
+ 'Do not exclude files that match PAT.') { |val|
327
+ $diff.include << val
328
+ }
329
+ opts.on('-S FILE', '--starting-file=FILE',
330
+ 'Start with FILE when comparing directories.') { |val|
331
+ set_flag('-S', val)
332
+ }
333
+ opts.on('--from-file=FILE1',
334
+ 'Compare FILE1 to all operands. FILE1 can be a directory.') { |val|
335
+ $diff.from_files = [val]
336
+ }
337
+ opts.on('--to-file=FILE2',
338
+ 'Compare all operands to FILE2. FILE2 can be a directory.') { |val|
339
+ $diff.to_files = [val]
340
+ }
341
+ opts.on('--horizon-lines=NUM', Integer,
342
+ 'Keep NUM lines of the common prefix and suffix.') { |val|
343
+ set_flag('--horizon-lines', val.to_s)
344
+ }
345
+ opts.on('-d', '--minimal[=-]', miniTrueClass,
346
+ 'Try hard to find a smaller set of changes. [!]') { |val|
347
+ set_flag('-d', val)
348
+ }
349
+ opts.on('--speed-large-files[=-]', miniTrueClass,
350
+ 'Assume large files and many scattered small changes.') { |val|
351
+ set_flag('--speed-large-files', val)
352
+ }
353
+ opts.on('-v', '--version',
354
+ 'Output version info.') { |val|
355
+ print <<-"EOF"
356
+ #{MYNAME} version #{MYVERSION}
357
+ #{MYCOPYRIGHT}
358
+
359
+ ----
360
+ EOF
361
+ system(DIFF_CMD, '--version')
362
+ exit
363
+ }
364
+ opts.on('--help',
365
+ 'Output this help.') { |val|
366
+ invoke_pager
367
+ print opts, <<EOS
368
+ Options without the [*] sign will be passed through to diff(1).
369
+ Options marked as [!] sign are turned on by default. To turn them off,
370
+ specify -?- for short options and --no-??? for long options, respectively.
371
+
372
+ Environment variables:
373
+ EOS
374
+ [
375
+ ['DIFF', 'Path to diff(1)'],
376
+ [ENV_NAME, 'User\'s preferred default options'],
377
+ ['PAGER', 'Path to pager (more(1) is used if not defined)'],
378
+ ].each { |name, description|
379
+ printf " %-14s %s\n", name, description
380
+ }
381
+ exit 0
382
+ }
383
+ }
384
+
385
+ begin
386
+ opts.parse('--rsync-exclude', '--fignore-exclude', '--ignore-cvs-lines',
387
+ '--pager', '--color',
388
+ '-U3', '-N', '-r', '-p', '-d')
389
+
390
+ if value = ENV[ENV_NAME]
391
+ require 'shellwords'
392
+ opts.parse(*value.shellsplit)
393
+ end
394
+
395
+ opts.parse!(args)
396
+
397
+ $diff.format_flags.each { |format_flag|
398
+ set_flag(*format_flag)
399
+ }
400
+
401
+ if $diff.ignore_cvs_lines
402
+ opts.parse('--ignore-matching-lines="^[^\x1b]*\$[A-Z][A-Za-z0-9][A-Za-z0-9]*\(:.*\)\{0,1\}\$')
403
+ end
404
+ rescue OptionParser::ParseError => e
405
+ warn e, "Try `#{MYNAME} --help' for more information."
406
+ exit 64
407
+ rescue => e
408
+ warn e
409
+ exit 1
410
+ end
411
+
412
+ begin
413
+ if $diff.from_files
414
+ $diff.to_files ||= args.dup
415
+
416
+ if $diff.to_files.empty?
417
+ raise "missing operand"
418
+ end
419
+ elsif $diff.to_files
420
+ $diff.from_files = args.dup
421
+
422
+ if $diff.from_files.empty?
423
+ raise "missing operand"
424
+ end
425
+ else
426
+ if args.size < 2
427
+ raise "missing operand"
428
+ end
429
+
430
+ if File.directory?(args[0])
431
+ $diff.to_files = args.dup
432
+ $diff.from_files = [$diff.to_files.shift]
433
+ else
434
+ $diff.from_files = args.dup
435
+ $diff.to_files = [$diff.from_files.pop]
436
+ end
437
+ end
438
+
439
+ if $diff.from_files.size != 1 && $diff.to_files.size != 1
440
+ raise "wrong number of files given"
441
+ end
442
+ rescue => e
443
+ warn e, "Try `#{MYNAME} --help' for more information."
444
+ exit 64
445
+ end
446
+ end
447
+
448
+ def invoke_pager
449
+ invoke_pager! if $diff.use_pager
450
+ end
451
+
452
+ def invoke_pager!
453
+ $stdout.flush
454
+ $stderr.flush
455
+ pr, pw = IO.pipe
456
+ ppid = Process.pid
457
+ pid = fork {
458
+ $stdin.reopen(pr)
459
+ pr.close
460
+ pw.close
461
+ IO.select([$stdin], nil, [$stdin])
462
+ if system(ENV['PAGER'] || 'more')
463
+ Process.kill(:TERM, ppid)
464
+ else
465
+ $stderr.puts "Pager failed."
466
+ Process.kill(:INT, ppid)
467
+ end
468
+ }
469
+ trap(:TERM) { exit(0) }
470
+ trap(:INT) { exit(130) }
471
+ $stdout.reopen(pw)
472
+ $stderr.reopen(pw) if $stderr.tty?
473
+ pw.close
474
+ at_exit {
475
+ $stdout.flush
476
+ $stderr.flush
477
+ $stdout.close
478
+ $stderr.close
479
+ Process.waitpid(pid)
480
+ }
481
+ end
482
+
483
+ def set_flag(flag, val)
484
+ case val
485
+ when false
486
+ $diff.flags.reject! { |f,| f == flag }
487
+ when true
488
+ $diff.flags.reject! { |f,| f == flag }
489
+ $diff.flags << [flag]
490
+ else
491
+ $diff.flags << [flag, val]
492
+ end
493
+ end
494
+
495
+ def set_format_flag(flag, *val)
496
+ $diff.format_flags.clear
497
+ $diff.custom_format_p = false
498
+ case flag
499
+ when '-C'
500
+ $diff.format = :context
501
+ when '-U'
502
+ $diff.format = :unified
503
+ when '-e'
504
+ $diff.format = :ed
505
+ when '--normal'
506
+ $diff.format = :normal
507
+ when '-n'
508
+ $diff.format = :rcs
509
+ when '-y'
510
+ $diff.format = :side_by_side
511
+ when '-D'
512
+ $diff.format = :ifdef
513
+ else
514
+ $diff.format = :unknown
515
+ end
516
+ $diff.format_flags.push([flag, *val])
517
+ end
518
+
519
+ def set_custom_format_flag(flag, *val)
520
+ if !$diff.custom_format_p
521
+ $diff.format_flags.clear
522
+ $diff.custom_format_p = true
523
+ end
524
+ $diff.format = :custom
525
+ $diff.format_flags.push([flag, *val])
526
+ end
527
+
528
+ def diff_main
529
+ invoke_pager
530
+
531
+ $status = 0
532
+
533
+ $diff.from_files.each { |from_file|
534
+ if File.directory?(from_file)
535
+ $diff.to_files.each { |to_file|
536
+ if File.directory?(to_file)
537
+ if $diff.relative
538
+ to_file = File.expand_path(from_file, to_file)
539
+ end
540
+
541
+ diff_dirs(from_file, to_file)
542
+ else
543
+ if $diff.relative
544
+ from_file = File.expand_path(to_file, from_file)
545
+ else
546
+ from_file = File.expand_path(File.basename(to_file), from_file)
547
+ end
548
+
549
+ diff_files(from_file, to_file)
550
+ end
551
+ }
552
+ else
553
+ $diff.to_files.each { |to_file|
554
+ if File.directory?(to_file)
555
+ if $diff.relative
556
+ to_file = File.expand_path(from_file, to_file)
557
+ else
558
+ to_file = File.expand_path(File.basename(from_file), to_file)
559
+ end
560
+ end
561
+
562
+ diff_files(from_file, to_file)
563
+ }
564
+ end
565
+ }
566
+ end
567
+
568
+ def diff_files(file1, file2)
569
+ if file1.is_a?(Array)
570
+ file2.is_a?(Array) and raise "cannot compare two sets of multiple files"
571
+ file1.empty? and return 0
572
+
573
+ call_diff('--to-file', file2, '--', file1)
574
+ elsif file2.is_a?(Array)
575
+ file1.empty? and return 0
576
+
577
+ call_diff('--from-file', file1, '--', file2)
578
+ else
579
+ call_diff('--', file1, file2)
580
+ end
581
+ end
582
+
583
+ def call_diff(*args)
584
+ command_args = [DIFF_CMD, $diff.flags, args].flatten
585
+ if $diff.colorize
586
+ case $diff.format
587
+ when :unified
588
+ filter = method(:colorize_unified_diff)
589
+ when :context
590
+ filter = method(:colorize_context_diff)
591
+ end
592
+ end
593
+ if filter
594
+ require 'shellwords'
595
+ filter.call(IO.popen(command_args.shelljoin, 'r'))
596
+ else
597
+ system(*command_args)
598
+ end
599
+ status = $? >> 8
600
+ $status = status if $status < status
601
+ return status
602
+ end
603
+
604
+ def diff_dirs(dir1, dir2)
605
+ entries1 = diff_entries(dir1)
606
+ entries2 = diff_entries(dir2)
607
+
608
+ common = entries1 & entries2
609
+ missing1 = entries2 - entries1
610
+ missing2 = entries1 - entries2
611
+
612
+ files = []
613
+ common.each { |file|
614
+ file1 = File.join(dir1, file)
615
+ file2 = File.join(dir2, file)
616
+ file1_is_dir = File.directory?(file1)
617
+ file2_is_dir = File.directory?(file2)
618
+ if file1_is_dir && file2_is_dir
619
+ diff_dirs(file1, file2) if $diff.recursive
620
+ elsif !file1_is_dir && !file2_is_dir
621
+ files << file1
622
+ else
623
+ missing1 << file
624
+ missing2 << file
625
+ end
626
+ }
627
+ diff_files(files, dir2)
628
+
629
+ [[dir1, missing2], [dir2, missing1]].each { |dir, missing|
630
+ new_files = []
631
+ missing.each { |entry|
632
+ file = File.join(dir, entry)
633
+
634
+ if $diff.new_file
635
+ if File.directory?(file)
636
+ if dir.equal?(dir1)
637
+ diff_dirs(file, nil)
638
+ else
639
+ diff_dirs(nil, file)
640
+ end
641
+ else
642
+ new_files << file
643
+ end
644
+ else
645
+ printf "Only in %s: %s (%s)\n",
646
+ dir, entry, File.directory?(file) ? 'directory' : 'file'
647
+ $status = 1 if $status < 1
648
+ end
649
+ }
650
+ if dir.equal?(dir1)
651
+ diff_files(new_files, EMPTYFILE)
652
+ else
653
+ diff_files(EMPTYFILE, new_files)
654
+ end
655
+ }
656
+ end
657
+
658
+ def diff_entries(dir)
659
+ return [] if dir.nil?
660
+ return Dir.entries(dir).reject { |file| diff_exclude?(file) }
661
+ rescue => e
662
+ warn "#{dir}: #{e}"
663
+ return []
664
+ end
665
+
666
+ def diff_exclude?(basename)
667
+ return true if basename == '.' || basename == '..'
668
+ return false if $diff.include.any? { |pat|
669
+ File.fnmatch(pat, basename, File::FNM_DOTMATCH)
670
+ }
671
+ return true if $diff.exclude.any? { |pat|
672
+ File.fnmatch(pat, basename, File::FNM_DOTMATCH)
673
+ }
674
+ return true if $diff.rsync_exclude && RSYNC_EXCLUDE_GLOBS.any? { |pat|
675
+ File.fnmatch(pat, basename, File::FNM_DOTMATCH)
676
+ }
677
+ return true if $diff.fignore_exclude && FIGNORE_GLOBS.any? { |pat|
678
+ File.fnmatch(pat, basename, File::FNM_DOTMATCH)
679
+ }
680
+ return false
681
+ end
682
+
683
+ def colorize_unified_diff(io)
684
+ colors = $diff.colors
685
+
686
+ state = :comment
687
+ hunk_left = nil
688
+ io.each_line { |line|
689
+ case state
690
+ when :comment
691
+ case line
692
+ when /^\+{3} /
693
+ color = colors[:file1]
694
+ when /^-{3} /
695
+ color = colors[:file2]
696
+ when /^@@ -[0-9]+(,([0-9]+))? \+[0-9]+(,([0-9]+))?/
697
+ state = :hunk
698
+ hunk_left = ($1 ? $2.to_i : 1) + ($3 ? $4.to_i : 1)
699
+ line.sub!(/^(@@ .*? @@)( )?/) {
700
+ $1 + ($2 ? colors[:off] + $2 + colors[:function] : '')
701
+ }
702
+ color = colors[:header]
703
+ else
704
+ color = colors[:comment]
705
+ end
706
+ when :hunk
707
+ check = false
708
+ case line
709
+ when /^\+/
710
+ color = colors[:new]
711
+ hunk_left -= 1
712
+ check = $diff.highlight_whitespace
713
+ when /^-/
714
+ color = colors[:old]
715
+ hunk_left -= 1
716
+ check = $diff.highlight_whitespace
717
+ when /^ /
718
+ color = colors[:unchanged]
719
+ hunk_left -= 2
720
+ else
721
+ # error
722
+ color = colors[:comment]
723
+ end
724
+ if check
725
+ line.sub!(/([ \t]+)$/) {
726
+ colors[:off] + colors[:whitespace] + $1
727
+ }
728
+ end
729
+ if hunk_left <= 0
730
+ state = :comment
731
+ hunk_left = nil
732
+ end
733
+ end
734
+
735
+ line.sub!(/^/, color)
736
+ line.sub!(/$/, colors[:off])
737
+
738
+ print line
739
+ }
740
+
741
+ io.close
742
+ end
743
+
744
+ def colorize_context_diff(io)
745
+ colors = $diff.colors
746
+
747
+ state = :comment
748
+ hunk_part = nil
749
+ io.each_line { |line|
750
+ case state
751
+ when :comment
752
+ case line
753
+ when /^\*{3} /
754
+ color = colors[:file1]
755
+ when /^-{3} /
756
+ color = colors[:file2]
757
+ when /^\*{15}/
758
+ state = :hunk
759
+ hunk_part = 0
760
+ line.sub!(/^(\*{15})( )?/) {
761
+ $1 + ($2 ? colors[:off] + $2 + colors[:function] : '')
762
+ }
763
+ color = colors[:header]
764
+ end
765
+ when :hunk
766
+ case hunk_part
767
+ when 0
768
+ case line
769
+ when /^\*{3} /
770
+ hunk_part = 1
771
+ color = colors[:header]
772
+ else
773
+ # error
774
+ color = colors[:comment]
775
+ end
776
+ when 1, 2
777
+ check = false
778
+ case line
779
+ when /^\-{3} /
780
+ if hunk_part == 1
781
+ hunk_part = 2
782
+ color = colors[:header]
783
+ else
784
+ #error
785
+ color = colors[:comment]
786
+ end
787
+ when /^\*{3} /, /^\*{15} /
788
+ state = :comment
789
+ redo
790
+ when /^\+ /
791
+ color = colors[:new]
792
+ check = $diff.highlight_whitespace
793
+ when /^- /
794
+ color = colors[:old]
795
+ check = $diff.highlight_whitespace
796
+ when /^! /
797
+ color = colors[:changed]
798
+ check = $diff.highlight_whitespace
799
+ when /^ /
800
+ color = colors[:unchanged]
801
+ else
802
+ # error
803
+ color = colors[:comment]
804
+ end
805
+ if check
806
+ line.sub!(/^(...*)([ \t]+)$/) {
807
+ $1 + colors[:off] + colors[:whitespace] + $2
808
+ }
809
+ end
810
+ end
811
+ end
812
+
813
+ line.sub!(/^/, color)
814
+ line.sub!(/$/, colors[:off])
815
+
816
+ print line
817
+ }
818
+
819
+ io.close
820
+ end
821
+
822
+ main(ARGV) if $0 == __FILE__