bringit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ module Bringit
2
+ class Compare
3
+ attr_reader :head, :base, :straight
4
+
5
+ def initialize(repository, base, head, straight = false)
6
+ @repository = repository
7
+ @straight = straight
8
+
9
+ unless base && head
10
+ @commits = []
11
+ return
12
+ end
13
+
14
+ @base = Bringit::Commit.find(repository, base.try(:strip))
15
+ @head = Bringit::Commit.find(repository, head.try(:strip))
16
+
17
+ @commits = [] unless @base && @head
18
+ @commits = [] if same
19
+ end
20
+
21
+ def same
22
+ @base && @head && @base.id == @head.id
23
+ end
24
+
25
+ def commits
26
+ return @commits if defined?(@commits)
27
+
28
+ @commits = Bringit::Commit.between(@repository, @base.id, @head.id)
29
+ end
30
+
31
+ def diffs(options = {})
32
+ unless @head && @base
33
+ return Bringit::DiffCollection.new([])
34
+ end
35
+
36
+ paths = options.delete(:paths) || []
37
+ options[:straight] = @straight
38
+ Bringit::Diff.between(@repository, @head.id, @base.id, options, *paths)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,320 @@
1
+ # Bringit::Diff is a wrapper around native Rugged::Diff object
2
+ module Bringit
3
+ class Diff
4
+ TimeoutError = Class.new(StandardError)
5
+ include Bringit::EncodingHelper
6
+
7
+ # Diff properties
8
+ attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
9
+
10
+ # Stats properties
11
+ attr_accessor :new_file, :renamed_file, :deleted_file
12
+
13
+ attr_accessor :too_large
14
+
15
+ # The maximum size of a diff to display.
16
+ DIFF_SIZE_LIMIT = 102400 # 100 KB
17
+
18
+ # The maximum size before a diff is collapsed.
19
+ DIFF_COLLAPSE_LIMIT = 10240 # 10 KB
20
+
21
+ class << self
22
+ def between(repo, head, base, options = {}, *paths)
23
+ straight = options.delete(:straight) || false
24
+
25
+ common_commit = if straight
26
+ base
27
+ else
28
+ # Only show what is new in the source branch
29
+ # compared to the target branch, not the other way
30
+ # around. The linex below with merge_base is
31
+ # equivalent to diff with three dots (git diff
32
+ # branch1...branch2) From the git documentation:
33
+ # "git diff A...B" is equivalent to "git diff
34
+ # $(git-merge-base A B) B"
35
+ repo.merge_base_commit(head, base)
36
+ end
37
+
38
+ options ||= {}
39
+ actual_options = filter_diff_options(options)
40
+ repo.diff(common_commit, head, actual_options, *paths)
41
+ end
42
+
43
+ # Return a copy of the +options+ hash containing only keys that can be
44
+ # passed to Rugged. Allowed options are:
45
+ #
46
+ # :max_size ::
47
+ # An integer specifying the maximum byte size of a file before a it
48
+ # will be treated as binary. The default value is 512MB.
49
+ #
50
+ # :context_lines ::
51
+ # The number of unchanged lines that define the boundary of a hunk
52
+ # (and to display before and after the actual changes). The default is
53
+ # 3.
54
+ #
55
+ # :interhunk_lines ::
56
+ # The maximum number of unchanged lines between hunk boundaries before
57
+ # the hunks will be merged into a one. The default is 0.
58
+ #
59
+ # :old_prefix ::
60
+ # The virtual "directory" to prefix to old filenames in hunk headers.
61
+ # The default is "a".
62
+ #
63
+ # :new_prefix ::
64
+ # The virtual "directory" to prefix to new filenames in hunk headers.
65
+ # The default is "b".
66
+ #
67
+ # :reverse ::
68
+ # If true, the sides of the diff will be reversed.
69
+ #
70
+ # :force_text ::
71
+ # If true, all files will be treated as text, disabling binary
72
+ # attributes & detection.
73
+ #
74
+ # :ignore_whitespace ::
75
+ # If true, all whitespace will be ignored.
76
+ #
77
+ # :ignore_whitespace_change ::
78
+ # If true, changes in amount of whitespace will be ignored.
79
+ #
80
+ # :ignore_whitespace_eol ::
81
+ # If true, whitespace at end of line will be ignored.
82
+ #
83
+ # :ignore_submodules ::
84
+ # if true, submodules will be excluded from the diff completely.
85
+ #
86
+ # :patience ::
87
+ # If true, the "patience diff" algorithm will be used (currenlty
88
+ # unimplemented).
89
+ #
90
+ # :include_ignored ::
91
+ # If true, ignored files will be included in the diff.
92
+ #
93
+ # :include_untracked ::
94
+ # If true, untracked files will be included in the diff.
95
+ #
96
+ # :include_unmodified ::
97
+ # If true, unmodified files will be included in the diff.
98
+ #
99
+ # :recurse_untracked_dirs ::
100
+ # Even if +:include_untracked+ is true, untracked directories will
101
+ # only be marked with a single entry in the diff. If this flag is set
102
+ # to true, all files under ignored directories will be included in the
103
+ # diff, too.
104
+ #
105
+ # :disable_pathspec_match ::
106
+ # If true, the given +*paths+ will be applied as exact matches,
107
+ # instead of as fnmatch patterns.
108
+ #
109
+ # :deltas_are_icase ::
110
+ # If true, filename comparisons will be made with case-insensitivity.
111
+ #
112
+ # :include_untracked_content ::
113
+ # if true, untracked content will be contained in the the diff patch
114
+ # text.
115
+ #
116
+ # :skip_binary_check ::
117
+ # If true, diff deltas will be generated without spending time on
118
+ # binary detection. This is useful to improve performance in cases
119
+ # where the actual file content difference is not needed.
120
+ #
121
+ # :include_typechange ::
122
+ # If true, type changes for files will not be interpreted as deletion
123
+ # of the "old file" and addition of the "new file", but will generate
124
+ # typechange records.
125
+ #
126
+ # :include_typechange_trees ::
127
+ # Even if +:include_typechange+ is true, blob -> tree changes will
128
+ # still usually be handled as a deletion of the blob. If this flag is
129
+ # set to true, blob -> tree changes will be marked as typechanges.
130
+ #
131
+ # :ignore_filemode ::
132
+ # If true, file mode changes will be ignored.
133
+ #
134
+ # :recurse_ignored_dirs ::
135
+ # Even if +:include_ignored+ is true, ignored directories will only be
136
+ # marked with a single entry in the diff. If this flag is set to true,
137
+ # all files under ignored directories will be included in the diff,
138
+ # too.
139
+ def filter_diff_options(options, default_options = {})
140
+ allowed_options = [:max_size, :context_lines, :interhunk_lines,
141
+ :old_prefix, :new_prefix, :reverse, :force_text,
142
+ :ignore_whitespace, :ignore_whitespace_change,
143
+ :ignore_whitespace_eol, :ignore_submodules,
144
+ :patience, :include_ignored, :include_untracked,
145
+ :include_unmodified, :recurse_untracked_dirs,
146
+ :disable_pathspec_match, :deltas_are_icase,
147
+ :include_untracked_content, :skip_binary_check,
148
+ :include_typechange, :include_typechange_trees,
149
+ :ignore_filemode, :recurse_ignored_dirs, :paths,
150
+ :max_files, :max_lines, :all_diffs, :no_collapse]
151
+
152
+ if default_options
153
+ actual_defaults = default_options.dup
154
+ actual_defaults.keep_if do |key|
155
+ allowed_options.include?(key)
156
+ end
157
+ else
158
+ actual_defaults = {}
159
+ end
160
+
161
+ if options
162
+ filtered_opts = options.dup
163
+ filtered_opts.keep_if do |key|
164
+ allowed_options.include?(key)
165
+ end
166
+ filtered_opts = actual_defaults.merge(filtered_opts)
167
+ else
168
+ filtered_opts = actual_defaults
169
+ end
170
+
171
+ filtered_opts
172
+ end
173
+ end
174
+
175
+ def initialize(raw_diff, collapse: false)
176
+ case raw_diff
177
+ when Hash
178
+ init_from_hash(raw_diff, collapse: collapse)
179
+ when Rugged::Patch, Rugged::Diff::Delta
180
+ init_from_rugged(raw_diff, collapse: collapse)
181
+ when nil
182
+ raise "Nil as raw diff passed"
183
+ else
184
+ raise "Invalid raw diff type: #{raw_diff.class}"
185
+ end
186
+ end
187
+
188
+ def serialize_keys
189
+ @serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large)
190
+ end
191
+
192
+ def to_hash
193
+ hash = {}
194
+
195
+ keys = serialize_keys
196
+
197
+ keys.each do |key|
198
+ hash[key] = send(key)
199
+ end
200
+
201
+ hash
202
+ end
203
+
204
+ def submodule?
205
+ a_mode == '160000' || b_mode == '160000'
206
+ end
207
+
208
+ def line_count
209
+ @line_count ||= Util.count_lines(@diff)
210
+ end
211
+
212
+ def too_large?
213
+ if @too_large.nil?
214
+ @too_large = @diff.bytesize >= DIFF_SIZE_LIMIT
215
+ else
216
+ @too_large
217
+ end
218
+ end
219
+
220
+ def collapsible?
221
+ @diff.bytesize >= DIFF_COLLAPSE_LIMIT
222
+ end
223
+
224
+ def prune_large_diff!
225
+ @diff = ''
226
+ @line_count = 0
227
+ @too_large = true
228
+ end
229
+
230
+ def collapsed?
231
+ return @collapsed if defined?(@collapsed)
232
+ false
233
+ end
234
+
235
+ def prune_collapsed_diff!
236
+ @diff = ''
237
+ @line_count = 0
238
+ @collapsed = true
239
+ end
240
+
241
+ private
242
+
243
+ def init_from_rugged(rugged, collapse: false)
244
+ if rugged.is_a?(Rugged::Patch)
245
+ init_from_rugged_patch(rugged, collapse: collapse)
246
+ d = rugged.delta
247
+ else
248
+ d = rugged
249
+ end
250
+
251
+ @new_path = encode!(d.new_file[:path])
252
+ @old_path = encode!(d.old_file[:path])
253
+ @a_mode = d.old_file[:mode].to_s(8)
254
+ @b_mode = d.new_file[:mode].to_s(8)
255
+ @new_file = d.added?
256
+ @renamed_file = d.renamed?
257
+ @deleted_file = d.deleted?
258
+ end
259
+
260
+ def init_from_rugged_patch(patch, collapse: false)
261
+ # Don't bother initializing diffs that are too large. If a diff is
262
+ # binary we're not going to display anything so we skip the size check.
263
+ return if !patch.delta.binary? && prune_large_patch(patch, collapse)
264
+
265
+ @diff = encode!(strip_diff_headers(patch.to_s))
266
+ end
267
+
268
+ def init_from_hash(hash, collapse: false)
269
+ raw_diff = hash.symbolize_keys
270
+
271
+ serialize_keys.each do |key|
272
+ send(:"#{key}=", raw_diff[key.to_sym])
273
+ end
274
+
275
+ prune_large_diff! if too_large?
276
+ prune_collapsed_diff! if collapse && collapsible?
277
+ end
278
+
279
+ # If the patch surpasses any of the diff limits it calls the appropiate
280
+ # prune method and returns true. Otherwise returns false.
281
+ def prune_large_patch(patch, collapse)
282
+ size = 0
283
+
284
+ patch.each_hunk do |hunk|
285
+ hunk.each_line do |line|
286
+ size += line.content.bytesize
287
+
288
+ if size >= DIFF_SIZE_LIMIT
289
+ prune_large_diff!
290
+ return true
291
+ end
292
+ end
293
+ end
294
+
295
+ if collapse && size >= DIFF_COLLAPSE_LIMIT
296
+ prune_collapsed_diff!
297
+ return true
298
+ end
299
+
300
+ false
301
+ end
302
+
303
+ # Strip out the information at the beginning of the patch's text to match
304
+ # Grit's output
305
+ def strip_diff_headers(diff_text)
306
+ # Delete everything up to the first line that starts with '---' or
307
+ # 'Binary'
308
+ diff_text.sub!(/\A.*?^(---|Binary)/m, '\1')
309
+
310
+ if diff_text.start_with?('---', 'Binary')
311
+ diff_text
312
+ else
313
+ # If the diff_text did not contain a line starting with '---' or
314
+ # 'Binary', return the empty string. No idea why; we are just
315
+ # preserving behavior from before the refactor.
316
+ ''
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,127 @@
1
+ module Bringit
2
+ class DiffCollection
3
+ include Enumerable
4
+
5
+ DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze
6
+
7
+ def initialize(iterator, options = {})
8
+ @iterator = iterator
9
+ @max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
10
+ @max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
11
+ @max_bytes = @max_files * 5120 # Average 5 KB per file
12
+ @safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min
13
+ @safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min
14
+ @safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
15
+ @all_diffs = !!options.fetch(:all_diffs, false)
16
+ @no_collapse = !!options.fetch(:no_collapse, true)
17
+ @deltas_only = !!options.fetch(:deltas_only, false)
18
+
19
+ @line_count = 0
20
+ @byte_count = 0
21
+ @overflow = false
22
+ @array = Array.new
23
+ end
24
+
25
+ def each(&block)
26
+ if @populated
27
+ # @iterator.each is slower than just iterating the array in place
28
+ @array.each(&block)
29
+ elsif @deltas_only
30
+ each_delta(&block)
31
+ else
32
+ each_patch(&block)
33
+ end
34
+ end
35
+
36
+ def empty?
37
+ !@iterator.any?
38
+ end
39
+
40
+ def overflow?
41
+ populate!
42
+ !!@overflow
43
+ end
44
+
45
+ def size
46
+ @size ||= count # forces a loop using each method
47
+ end
48
+
49
+ def real_size
50
+ populate!
51
+
52
+ if @overflow
53
+ "#{size}+"
54
+ else
55
+ size.to_s
56
+ end
57
+ end
58
+
59
+ def decorate!
60
+ collection = each_with_index do |element, i|
61
+ @array[i] = yield(element)
62
+ end
63
+ @populated = true
64
+ collection
65
+ end
66
+
67
+ private
68
+
69
+ def populate!
70
+ return if @populated
71
+
72
+ each { nil } # force a loop through all diffs
73
+ @populated = true
74
+ nil
75
+ end
76
+
77
+ def over_safe_limits?(files)
78
+ files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes
79
+ end
80
+
81
+ def each_delta
82
+ @iterator.each_delta.with_index do |delta, i|
83
+ diff = Bringit::Diff.new(delta)
84
+
85
+ yield @array[i] = diff
86
+ end
87
+ end
88
+
89
+ def each_patch
90
+ @iterator.each_with_index do |raw, i|
91
+ # First yield cached Diff instances from @array
92
+ if @array[i]
93
+ yield @array[i]
94
+ next
95
+ end
96
+
97
+ # We have exhausted @array, time to create new Diff instances or stop.
98
+ break if @overflow
99
+
100
+ if !@all_diffs && i >= @max_files
101
+ @overflow = true
102
+ break
103
+ end
104
+
105
+ collapse = !@all_diffs && !@no_collapse
106
+
107
+ diff = Bringit::Diff.new(raw, collapse: collapse)
108
+
109
+ if collapse && over_safe_limits?(i)
110
+ diff.prune_collapsed_diff!
111
+ end
112
+
113
+ @line_count += diff.line_count
114
+ @byte_count += diff.diff.bytesize
115
+
116
+ if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes)
117
+ # This last Diff instance pushes us over the lines limit. We stop and
118
+ # discard it.
119
+ @overflow = true
120
+ break
121
+ end
122
+
123
+ yield @array[i] = diff
124
+ end
125
+ end
126
+ end
127
+ end