bringit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ module Bringit
2
+ module EncodingHelper
3
+ extend self
4
+
5
+ # This threshold is carefully tweaked to prevent usage of encodings detected
6
+ # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
7
+ # we're better off sticking with utf8 encoding.
8
+ # Reason: git diff can return strings with invalid utf8 byte sequences if it
9
+ # truncates a diff in the middle of a multibyte character. In this case
10
+ # CharlockHolmes will try to guess the encoding and will likely suggest an
11
+ # obscure encoding with low confidence.
12
+ # There is a lot more info with this merge request:
13
+ # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
14
+ ENCODING_CONFIDENCE_THRESHOLD = 40
15
+
16
+ def encode!(message)
17
+ return nil unless message.respond_to? :force_encoding
18
+
19
+ # if message is utf-8 encoding, just return it
20
+ message.force_encoding("UTF-8")
21
+ return message if message.valid_encoding?
22
+
23
+ # return message if message type is binary
24
+ detect = CharlockHolmes::EncodingDetector.detect(message)
25
+ return message.force_encoding("BINARY") if detect && detect[:type] == :binary
26
+
27
+ # force detected encoding if we have sufficient confidence.
28
+ if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
29
+ message.force_encoding(detect[:encoding])
30
+ end
31
+
32
+ # encode and clean the bad chars
33
+ message.replace clean(message)
34
+ rescue
35
+ encoding = detect ? detect[:encoding] : "unknown"
36
+ "--broken encoding: #{encoding}"
37
+ end
38
+
39
+ def encode_utf8(message)
40
+ detect = CharlockHolmes::EncodingDetector.detect(message)
41
+ if detect
42
+ CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
43
+ else
44
+ clean(message)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def clean(message)
51
+ message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
52
+ .encode("UTF-8")
53
+ .gsub("\0".encode("UTF-8"), "")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,87 @@
1
+ require "bundler"
2
+
3
+ module Bringit
4
+ class Hook
5
+ GL_PROTOCOL = 'web'.freeze
6
+ attr_reader :name, :repo_path, :path
7
+
8
+ def initialize(name, repo_path)
9
+ @name = name
10
+ @repo_path = repo_path
11
+ @path = File.join(repo_path.strip, 'hooks', name)
12
+ end
13
+
14
+ def exists?
15
+ File.exist?(path)
16
+ end
17
+
18
+ def trigger(user_id, oldrev, newrev, ref)
19
+ return [true, nil] unless exists?
20
+
21
+ Bundler.with_clean_env do
22
+ case name
23
+ when "pre-receive", "post-receive"
24
+ call_receive_hook(user_id, oldrev, newrev, ref)
25
+ when "update"
26
+ call_update_hook(user_id, oldrev, newrev, ref)
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def call_receive_hook(user_id, oldrev, newrev, ref)
34
+ changes = [oldrev, newrev, ref].join(" ")
35
+
36
+ exit_status = false
37
+ exit_message = nil
38
+
39
+ vars = {
40
+ 'USER_ID' => user_id,
41
+ 'PWD' => repo_path,
42
+ 'GL_PROTOCOL' => GL_PROTOCOL
43
+ }
44
+
45
+ options = {
46
+ chdir: repo_path
47
+ }
48
+
49
+ Open3.popen3(vars, path, options) do |stdin, stdout, stderr, wait_thr|
50
+ exit_status = true
51
+ stdin.sync = true
52
+
53
+ # in git, pre- and post- receive hooks may just exit without
54
+ # reading stdin. We catch the exception to avoid a broken pipe
55
+ # warning
56
+ begin
57
+ # inject all the changes as stdin to the hook
58
+ changes.lines do |line|
59
+ stdin.puts line
60
+ end
61
+ rescue Errno::EPIPE
62
+ end
63
+
64
+ stdin.close
65
+
66
+ unless wait_thr.value == 0
67
+ exit_status = false
68
+ exit_message = retrieve_error_message(stderr, stdout)
69
+ end
70
+ end
71
+
72
+ [exit_status, exit_message]
73
+ end
74
+
75
+ def call_update_hook(user_id, oldrev, newrev, ref)
76
+ Dir.chdir(repo_path) do
77
+ stdout, stderr, status = Open3.capture3({ 'USER_ID' => user_id }, path, ref, oldrev, newrev)
78
+ [status.success?, stderr.presence || stdout]
79
+ end
80
+ end
81
+
82
+ def retrieve_error_message(stderr, stdout)
83
+ err_message = stderr.gets
84
+ err_message.blank? ? stdout.gets : err_message
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,128 @@
1
+ module Bringit
2
+ class Index
3
+ DEFAULT_MODE = 0o100644
4
+
5
+ attr_reader :repository, :raw_index
6
+
7
+ def initialize(repository)
8
+ @repository = repository
9
+ @raw_index = repository.rugged.index
10
+ end
11
+
12
+ delegate :read_tree, :get, to: :raw_index
13
+
14
+ def write_tree
15
+ raw_index.write_tree(repository.rugged)
16
+ end
17
+
18
+ def dir_exists?(path)
19
+ raw_index.find { |entry| entry[:path].start_with?("#{path}/") }
20
+ end
21
+
22
+ def create(options)
23
+ options = normalize_options(options)
24
+
25
+ file_entry = get(options[:file_path])
26
+ if file_entry
27
+ raise Bringit::Repository::InvalidBlobName.new("Filename already exists")
28
+ end
29
+
30
+ add_blob(options)
31
+ end
32
+
33
+ def create_dir(options)
34
+ options = normalize_options(options)
35
+
36
+ file_entry = get(options[:file_path])
37
+ if file_entry
38
+ raise Bringit::Repository::InvalidBlobName.new("Directory already exists as a file")
39
+ end
40
+
41
+ if dir_exists?(options[:file_path])
42
+ raise Bringit::Repository::InvalidBlobName.new("Directory already exists")
43
+ end
44
+
45
+ options = options.dup
46
+ options[:file_path] += '/.gitkeep'
47
+ options[:content] = ''
48
+
49
+ add_blob(options)
50
+ end
51
+
52
+ def update(options)
53
+ options = normalize_options(options)
54
+
55
+ file_entry = get(options[:file_path])
56
+ unless file_entry
57
+ raise Bringit::Repository::InvalidBlobName.new("File doesn't exist")
58
+ end
59
+
60
+ add_blob(options, mode: file_entry[:mode])
61
+ end
62
+
63
+ def move(options)
64
+ options = normalize_options(options)
65
+
66
+ file_entry = get(options[:previous_path])
67
+ unless file_entry
68
+ raise Bringit::Repository::InvalidBlobName.new("File doesn't exist")
69
+ end
70
+
71
+ if get(options[:file_path])
72
+ raise IndexError, "A file with this name already exists"
73
+ end
74
+
75
+ raw_index.remove(options[:previous_path])
76
+
77
+ add_blob(options, mode: file_entry[:mode])
78
+ end
79
+
80
+ def delete(options)
81
+ options = normalize_options(options)
82
+
83
+ file_entry = get(options[:file_path])
84
+ unless file_entry
85
+ raise Bringit::Repository::InvalidBlobName.new("File doesn't exist")
86
+ end
87
+
88
+ raw_index.remove(options[:file_path])
89
+ end
90
+
91
+ private
92
+
93
+ def normalize_options(options)
94
+ options = options.dup
95
+ options[:file_path] = normalize_path(options[:file_path]) if options[:file_path]
96
+ options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path]
97
+ options
98
+ end
99
+
100
+ def normalize_path(path)
101
+ pathname = Bringit::PathHelper.normalize_path(path.dup)
102
+
103
+ if pathname.each_filename.include?('..')
104
+ raise Bringit::Repository::InvalidBlobName.new('Invalid path')
105
+ end
106
+
107
+ pathname.to_s
108
+ end
109
+
110
+ def add_blob(options, mode: nil)
111
+ content = options[:content]
112
+ content = Base64.decode64(content) if options[:encoding] == 'base64'
113
+
114
+ detect = CharlockHolmes::EncodingDetector.new.detect(content)
115
+ unless detect && detect[:type] == :binary
116
+ # When writing to the repo directly as we are doing here,
117
+ # the `core.autocrlf` config isn't taken into account.
118
+ content.gsub!("\r\n", "\n") if repository.autocrlf
119
+ end
120
+
121
+ oid = repository.rugged.write(content, :blob)
122
+
123
+ raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
124
+ rescue Rugged::IndexError => e
125
+ raise Bringit::Repository::InvalidBlobName.new(e.message)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,14 @@
1
+ module Bringit
2
+ class PathHelper
3
+ class << self
4
+ def normalize_path(filename)
5
+ # Strip all leading slashes so that //foo -> foo
6
+ filename = filename.sub(/^\/*/, '')
7
+
8
+ # Expand relative paths (e.g. foo/../bar)
9
+ filename = Pathname.new(filename)
10
+ filename.relative_path_from(Pathname.new(''))
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ require 'fileutils'
2
+ require 'open3'
3
+
4
+ module Bringit
5
+ module Popen
6
+ extend self
7
+
8
+ def self.popen(cmd, path = nil, vars = {})
9
+ unless cmd.is_a?(Array)
10
+ raise 'System commands must be given as an array of strings'
11
+ end
12
+
13
+ path ||= Dir.pwd
14
+ vars = vars.dup
15
+ vars['PWD'] = path
16
+ options = {chdir: path}
17
+
18
+ FileUtils.mkdir_p(path) unless File.directory?(path)
19
+
20
+ cmd_output = ''
21
+ cmd_status = 0
22
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
23
+ yield(stdin) if block_given?
24
+ stdin.close
25
+
26
+ cmd_output += stdout.read
27
+ cmd_output += stderr.read
28
+ cmd_status = wait_thr.value.exitstatus
29
+ end
30
+
31
+ [cmd_output, cmd_status]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bringit
4
+ # Methods for pulling
5
+ module Pulling
6
+ class Error < StandardError; end
7
+
8
+ def pull
9
+ if svn?
10
+ pull_svn
11
+ else
12
+ pull_git
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ def svn?
19
+ path.join('svn').directory?
20
+ end
21
+
22
+ def pull_svn
23
+ _out_fetch, status_fetch = Popen.popen(%w(git svn fetch), path.to_s)
24
+
25
+ ref = svn_has_trunk? ? 'trunk' : 'git-svn'
26
+ cmd = %W(git update-ref refs/heads/master refs/remotes/#{ref})
27
+ _out_update, status_update = Popen.popen(cmd, path.to_s)
28
+
29
+ [status_fetch, status_update].all?(&:zero?)
30
+ end
31
+
32
+ def pull_git
33
+ _out, status = Popen.popen(%w(git fetch --all), path.to_s)
34
+ status.zero?
35
+ end
36
+
37
+ def svn_has_trunk?
38
+ out, _status =
39
+ Popen.popen(%w(git config svn-remote.svn.fetch), path.to_s)
40
+ out.start_with?('trunk:')
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,56 @@
1
+ module Bringit
2
+ class Ref
3
+ include Bringit::EncodingHelper
4
+
5
+ def self.name_valid?(name)
6
+ if name.start_with?('refs/heads/') || name.start_with?('refs/remotes/')
7
+ return false
8
+ end
9
+
10
+ Popen.popen(%W(git check-ref-format refs/#{name})).last == 0
11
+ end
12
+
13
+
14
+ # Branch or tag name
15
+ # without "refs/tags|heads" prefix
16
+ attr_reader :name
17
+
18
+ # Target sha.
19
+ # Usually it is commit sha but in case
20
+ # when tag reference on other tag it can be tag sha
21
+ attr_reader :target
22
+
23
+ # Dereferenced target
24
+ # Commit object to which the Ref points to
25
+ attr_reader :dereferenced_target
26
+
27
+ # Extract branch name from full ref path
28
+ #
29
+ # Ex.
30
+ # Ref.extract_branch_name('refs/heads/master') #=> 'master'
31
+ def self.extract_branch_name(str)
32
+ str.gsub(/\Arefs\/heads\//, '')
33
+ end
34
+
35
+ def self.dereference_object(object)
36
+ object = object.target while object.is_a?(Rugged::Tag::Annotation)
37
+
38
+ object
39
+ end
40
+
41
+ def initialize(repository, name, target)
42
+ encode! name
43
+ @name = name.gsub(/\Arefs\/(tags|heads)\//, '')
44
+ @dereferenced_target = Bringit::Commit.find(repository, target)
45
+ @target = if target.respond_to?(:oid)
46
+ target.oid
47
+ elsif target.respond_to?(:name)
48
+ target.name
49
+ elsif target.is_a? String
50
+ target
51
+ else
52
+ nil
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,1230 @@
1
+ # Bringit::Repository is a wrapper around native Rugged::Repository object
2
+ require 'tempfile'
3
+ require 'forwardable'
4
+ require "rubygems/package"
5
+
6
+ module Bringit
7
+ class Repository
8
+ include Bringit::Popen
9
+
10
+ SEARCH_CONTEXT_LINES = 3
11
+
12
+ NoRepository = Class.new(StandardError)
13
+ InvalidBlobName = Class.new(StandardError)
14
+ InvalidRef = Class.new(StandardError)
15
+
16
+ # Full path to repo
17
+ attr_reader :path
18
+
19
+ # Directory name of repo
20
+ attr_reader :name
21
+
22
+ # Rugged repo object
23
+ attr_reader :rugged
24
+
25
+ # 'path' must be the path to a _bare_ git repository, e.g.
26
+ # /path/to/my-repo.git
27
+ def initialize(path)
28
+ @path = path
29
+ @name = path.split("/").last
30
+ @attributes = Bringit::Attributes.new(path)
31
+ end
32
+
33
+ delegate :empty?,
34
+ :bare?,
35
+ to: :rugged
36
+
37
+ # Default branch in the repository
38
+ def root_ref
39
+ @root_ref ||= discover_default_branch
40
+ end
41
+
42
+ # Alias to old method for compatibility
43
+ def raw
44
+ rugged
45
+ end
46
+
47
+ def rugged
48
+ @rugged ||= Rugged::Repository.new(path)
49
+ rescue Rugged::RepositoryError, Rugged::OSError
50
+ raise NoRepository.new('no repository for such path')
51
+ end
52
+
53
+ # Returns an Array of branch names
54
+ # sorted by name ASC
55
+ def branch_names
56
+ branches.map(&:name)
57
+ end
58
+
59
+ # Returns an Array of Branches
60
+ def branches
61
+ rugged.branches.map do |rugged_ref|
62
+ begin
63
+ Bringit::Branch.new(self, rugged_ref.name, rugged_ref.target)
64
+ rescue Rugged::ReferenceError
65
+ # Omit invalid branch
66
+ end
67
+ end.compact.sort_by(&:name)
68
+ end
69
+
70
+ def reload_rugged
71
+ @rugged = nil
72
+ end
73
+
74
+ # Directly find a branch with a simple name (e.g. master)
75
+ #
76
+ # force_reload causes a new Rugged repository to be instantiated
77
+ #
78
+ # This is to work around a bug in libgit2 that causes in-memory refs to
79
+ # be stale/invalid when packed-refs is changed.
80
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
81
+ def find_branch(name, force_reload = false)
82
+ reload_rugged if force_reload
83
+
84
+ rugged_ref = rugged.branches[name]
85
+ Bringit::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref
86
+ end
87
+
88
+ def local_branches
89
+ rugged.branches.each(:local).map do |branch|
90
+ Bringit::Branch.new(self, branch.name, branch.target)
91
+ end
92
+ end
93
+
94
+ # Returns the number of valid branches
95
+ def branch_count
96
+ rugged.branches.count do |ref|
97
+ begin
98
+ ref.name && ref.target # ensures the branch is valid
99
+
100
+ true
101
+ rescue Rugged::ReferenceError
102
+ false
103
+ end
104
+ end
105
+ end
106
+
107
+ # Returns an Array of tag names
108
+ def tag_names
109
+ rugged.tags.map { |t| t.name }
110
+ end
111
+
112
+ # Returns an Array of Tags
113
+ def tags
114
+ rugged.references.each("refs/tags/*").map do |ref|
115
+ message = nil
116
+
117
+ if ref.target.is_a?(Rugged::Tag::Annotation)
118
+ tag_message = ref.target.message
119
+
120
+ if tag_message.respond_to?(:chomp)
121
+ message = tag_message.chomp
122
+ end
123
+ end
124
+
125
+ Bringit::Tag.new(self, ref.name, ref.target, message)
126
+ end.sort_by(&:name)
127
+ end
128
+
129
+ # Returns true if the given tag exists
130
+ #
131
+ # name - The name of the tag as a String.
132
+ def tag_exists?(name)
133
+ !!rugged.tags[name]
134
+ end
135
+
136
+ # Returns true if the given branch exists
137
+ #
138
+ # name - The name of the branch as a String.
139
+ def branch_exists?(name)
140
+ rugged.branches.exists?(name)
141
+
142
+ # If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
143
+ # Whatever code calls this method shouldn't have to deal with that so
144
+ # instead we just return `false` (which is true since a branch doesn't
145
+ # exist when it has an invalid name).
146
+ rescue Rugged::ReferenceError
147
+ false
148
+ end
149
+
150
+ # Returns an Array of branch and tag names
151
+ def ref_names
152
+ branch_names + tag_names
153
+ end
154
+
155
+ # Deprecated. Will be removed in 5.2
156
+ def heads
157
+ rugged.references.each("refs/heads/*").map do |head|
158
+ Bringit::Ref.new(self, head.name, head.target)
159
+ end.sort_by(&:name)
160
+ end
161
+
162
+ def has_commits?
163
+ !empty?
164
+ end
165
+
166
+ def repo_exists?
167
+ !!rugged
168
+ end
169
+
170
+ # Discovers the default branch based on the repository's available branches
171
+ #
172
+ # - If no branches are present, returns nil
173
+ # - If one branch is present, returns its name
174
+ # - If two or more branches are present, returns current HEAD or master or first branch
175
+ def discover_default_branch
176
+ names = branch_names
177
+
178
+ return if names.empty?
179
+
180
+ return names[0] if names.length == 1
181
+
182
+ if rugged_head
183
+ extracted_name = Ref.extract_branch_name(rugged_head.name)
184
+
185
+ return extracted_name if names.include?(extracted_name)
186
+ end
187
+
188
+ if names.include?('master')
189
+ 'master'
190
+ else
191
+ names[0]
192
+ end
193
+ end
194
+
195
+ def rugged_head
196
+ rugged.head
197
+ rescue Rugged::ReferenceError
198
+ nil
199
+ end
200
+
201
+ def archive_prefix(ref, sha)
202
+ project_name = self.name.chomp('.git')
203
+ "#{project_name}-#{ref.tr('/', '-')}-#{sha}"
204
+ end
205
+
206
+ def archive_metadata(ref, storage_path, format = "tar.gz")
207
+ ref ||= root_ref
208
+ commit = Bringit::Commit.find(self, ref)
209
+ return {} if commit.nil?
210
+
211
+ prefix = archive_prefix(ref, commit.id)
212
+
213
+ {
214
+ 'RepoPath' => path,
215
+ 'ArchivePrefix' => prefix,
216
+ 'ArchivePath' => archive_file_path(prefix, storage_path, format),
217
+ 'CommitId' => commit.id,
218
+ }
219
+ end
220
+
221
+ def archive_file_path(name, storage_path, format = "tar.gz")
222
+ # Build file path
223
+ return nil unless name
224
+
225
+ extension =
226
+ case format
227
+ when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
228
+ "tar.bz2"
229
+ when "tar"
230
+ "tar"
231
+ when "zip"
232
+ "zip"
233
+ else
234
+ # everything else should fall back to tar.gz
235
+ "tar.gz"
236
+ end
237
+
238
+ file_name = "#{name}.#{extension}"
239
+ File.join(storage_path, self.name, file_name)
240
+ end
241
+
242
+ # Return repo size in megabytes
243
+ def size
244
+ size = Bringit::Popen.popen(%w(du -sk), path).first.strip.to_i
245
+ (size.to_f / 1024).round(2)
246
+ end
247
+
248
+ # Returns an array of BlobSnippets for files at the specified +ref+ that
249
+ # contain the +query+ string.
250
+ def search_files(query, ref = nil)
251
+ greps = []
252
+ ref ||= root_ref
253
+
254
+ populated_index(ref).each do |entry|
255
+ # Discard submodules
256
+ next if submodule?(entry)
257
+
258
+ blob = Bringit::Blob.raw(self, entry[:oid])
259
+
260
+ # Skip binary files
261
+ next if blob.data.encoding == Encoding::ASCII_8BIT
262
+
263
+ blob.load_all_data!
264
+ greps += build_greps(blob.data, query, ref, entry[:path])
265
+ end
266
+
267
+ greps
268
+ end
269
+
270
+ # Use the Rugged Walker API to build an array of commits.
271
+ #
272
+ # Usage.
273
+ # repo.log(
274
+ # ref: 'master',
275
+ # path: 'app/models',
276
+ # limit: 10,
277
+ # offset: 5,
278
+ # after: Time.new(2016, 4, 21, 14, 32, 10)
279
+ # )
280
+ #
281
+ def log(options)
282
+ default_options = {
283
+ limit: 10,
284
+ offset: 0,
285
+ path: nil,
286
+ follow: false,
287
+ skip_merges: false,
288
+ disable_walk: false,
289
+ after: nil,
290
+ before: nil,
291
+ unsafe_range: false,
292
+ }
293
+
294
+ options = default_options.merge(options)
295
+ options[:limit] ||= 0
296
+ options[:offset] ||= 0
297
+ actual_ref = options[:ref] || root_ref
298
+
299
+ if options[:unsafe_range]
300
+ log_by_shell(actual_ref, options)
301
+ else
302
+ begin
303
+ sha = sha_from_ref(actual_ref)
304
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
305
+ # Return an empty array if the ref wasn't found
306
+ return []
307
+ end
308
+ if log_using_shell?(options)
309
+ log_by_shell(sha, options)
310
+ else
311
+ log_by_walk(sha, options)
312
+ end
313
+ end
314
+ end
315
+
316
+ def log_using_shell?(options)
317
+ options[:path].present? ||
318
+ options[:disable_walk] ||
319
+ options[:skip_merges] ||
320
+ options[:after] ||
321
+ options[:before]
322
+ end
323
+
324
+ def log_by_walk(sha, options)
325
+ walk_options = {
326
+ show: sha,
327
+ sort: Rugged::SORT_TOPO,
328
+ limit: options[:limit],
329
+ offset: options[:offset]
330
+ }
331
+ commits = Rugged::Walker.walk(rugged, walk_options).to_a
332
+ if options[:only_commit_sha]
333
+ commits.map(&:oid)
334
+ else
335
+ commits
336
+ end
337
+ end
338
+
339
+ def log_by_shell(sha, options)
340
+ limit = options[:limit].to_i
341
+ offset = options[:offset].to_i
342
+ use_follow_flag = options[:follow] && options[:path].present?
343
+
344
+ # We will perform the offset in Ruby because --follow doesn't play well with --skip.
345
+ # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
346
+ offset_in_ruby = use_follow_flag && options[:offset].present?
347
+ limit += offset if offset_in_ruby
348
+
349
+ cmd = %W[git --git-dir=#{path} log]
350
+ cmd << "--max-count=#{limit}" if limit > 0
351
+ cmd << '--format=%H'
352
+ cmd << "--skip=#{offset}" unless offset_in_ruby
353
+ cmd << '--follow' if use_follow_flag
354
+ cmd << '--no-merges' if options[:skip_merges]
355
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
356
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
357
+ cmd << sha
358
+ if options[:path].present?
359
+ cmd += %W[-- #{options[:path].sub(%r{\A/*}, './')}]
360
+ end
361
+
362
+ raw_output = IO.popen(cmd) { |io| io.read }
363
+ lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
364
+
365
+ if options[:only_commit_sha]
366
+ lines.map(&:strip)
367
+ else
368
+ lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
369
+ end
370
+ end
371
+
372
+ def count_commits(options)
373
+ cmd = %W[git --git-dir=#{path} rev-list]
374
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
375
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
376
+ cmd += %W[--count #{options[:ref]}]
377
+ cmd += %W[-- #{options[:path]}] if options[:path].present?
378
+
379
+ raw_output = IO.popen(cmd) { |io| io.read }
380
+
381
+ raw_output.to_i
382
+ end
383
+
384
+ def sha_from_ref(ref)
385
+ rev_parse_target(ref).oid
386
+ end
387
+
388
+ # Return the object that +revspec+ points to. If +revspec+ is an
389
+ # annotated tag, then return the tag's target instead.
390
+ def rev_parse_target(revspec)
391
+ obj = rugged.rev_parse(revspec)
392
+ Ref.dereference_object(obj)
393
+ end
394
+
395
+ # Return a collection of Rugged::Commits between the two revspec arguments.
396
+ # See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for
397
+ # a detailed list of valid arguments.
398
+ def commits_between(from, to)
399
+ walker = Rugged::Walker.new(rugged)
400
+ walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
401
+
402
+ sha_from = sha_from_ref(from)
403
+ sha_to = sha_from_ref(to)
404
+
405
+ walker.push(sha_to)
406
+ walker.hide(sha_from)
407
+
408
+ commits = walker.to_a
409
+ walker.reset
410
+
411
+ commits
412
+ end
413
+
414
+ # Counts the amount of commits between `from` and `to`.
415
+ def count_commits_between(from, to)
416
+ commits_between(from, to).size
417
+ end
418
+
419
+ # Returns the SHA of the most recent common ancestor of +from+ and +to+
420
+ def merge_base_commit(from, to)
421
+ rugged.merge_base(from, to)
422
+ end
423
+
424
+ # Return an array of Diff objects that represent the diff
425
+ # between +from+ and +to+. See Diff::filter_diff_options for the allowed
426
+ # diff options. The +options+ hash can also include :break_rewrites to
427
+ # split larger rewrites into delete/add pairs.
428
+ def diff(from, to, options = {}, *paths)
429
+ Bringit::DiffCollection.new(diff_patches(from, to, options, *paths), options)
430
+ end
431
+
432
+ # Returns commits collection
433
+ #
434
+ # Ex.
435
+ # repo.find_commits(
436
+ # ref: 'master',
437
+ # max_count: 10,
438
+ # skip: 5,
439
+ # order: :date
440
+ # )
441
+ #
442
+ # +options+ is a Hash of optional arguments to git
443
+ # :ref is the ref from which to begin (SHA1 or name)
444
+ # :contains is the commit contained by the refs from which to begin (SHA1 or name)
445
+ # :max_count is the maximum number of commits to fetch
446
+ # :skip is the number of commits to skip
447
+ # :order is the commits order and allowed value is :date(default) or :topo
448
+ #
449
+ def find_commits(options = {})
450
+ actual_options = options.dup
451
+
452
+ allowed_options = [:ref, :max_count, :skip, :contains, :order]
453
+
454
+ actual_options.keep_if do |key|
455
+ allowed_options.include?(key)
456
+ end
457
+
458
+ default_options = { skip: 0 }
459
+ actual_options = default_options.merge(actual_options)
460
+
461
+ walker = Rugged::Walker.new(rugged)
462
+
463
+ if actual_options[:ref]
464
+ walker.push(rugged.rev_parse_oid(actual_options[:ref]))
465
+ elsif actual_options[:contains]
466
+ branches_contains(actual_options[:contains]).each do |branch|
467
+ walker.push(branch.target_id)
468
+ end
469
+ else
470
+ rugged.references.each("refs/heads/*") do |ref|
471
+ walker.push(ref.target_id)
472
+ end
473
+ end
474
+
475
+ if actual_options[:order] == :topo
476
+ walker.sorting(Rugged::SORT_TOPO)
477
+ else
478
+ walker.sorting(Rugged::SORT_DATE)
479
+ end
480
+
481
+ commits = []
482
+ offset = actual_options[:skip]
483
+ limit = actual_options[:max_count]
484
+ walker.each(offset: offset, limit: limit) do |commit|
485
+ bringit_commit = Bringit::Commit.decorate(commit, self)
486
+ commits.push(bringit_commit)
487
+ end
488
+
489
+ walker.reset
490
+
491
+ commits
492
+ rescue Rugged::OdbError
493
+ []
494
+ end
495
+
496
+ # Returns branch names collection that contains the special commit(SHA1
497
+ # or name)
498
+ #
499
+ # Ex.
500
+ # repo.branch_names_contains('master')
501
+ #
502
+ def branch_names_contains(commit)
503
+ branches_contains(commit).map { |c| c.name }
504
+ end
505
+
506
+ # Returns branch collection that contains the special commit(SHA1 or name)
507
+ #
508
+ # Ex.
509
+ # repo.branch_names_contains('master')
510
+ #
511
+ def branches_contains(commit)
512
+ commit_obj = rugged.rev_parse(commit)
513
+ parent = commit_obj.parents.first unless commit_obj.parents.empty?
514
+
515
+ walker = Rugged::Walker.new(rugged)
516
+
517
+ rugged.branches.select do |branch|
518
+ walker.push(branch.target_id)
519
+ walker.hide(parent) if parent
520
+ result = walker.any? { |c| c.oid == commit_obj.oid }
521
+ walker.reset
522
+
523
+ result
524
+ end
525
+ end
526
+
527
+ # Get refs hash which key is SHA1
528
+ # and value is a Rugged::Reference
529
+ def refs_hash
530
+ # Initialize only when first call
531
+ if @refs_hash.nil?
532
+ @refs_hash = Hash.new { |h, k| h[k] = [] }
533
+
534
+ rugged.references.each do |r|
535
+ # Symbolic/remote references may not have an OID; skip over them
536
+ target_oid = r.target.try(:oid)
537
+ if target_oid
538
+ sha = rev_parse_target(target_oid).oid
539
+ @refs_hash[sha] << r
540
+ end
541
+ end
542
+ end
543
+ @refs_hash
544
+ end
545
+
546
+ # Lookup for rugged object by oid or ref name
547
+ def lookup(oid_or_ref_name)
548
+ rugged.rev_parse(oid_or_ref_name)
549
+ end
550
+
551
+ # Return hash with submodules info for this repository
552
+ #
553
+ # Ex.
554
+ # {
555
+ # "rack" => {
556
+ # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320",
557
+ # "path" => "rack",
558
+ # "url" => "git://github.com/chneukirchen/rack.git"
559
+ # },
560
+ # "encoding" => {
561
+ # "id" => ....
562
+ # }
563
+ # }
564
+ #
565
+ def submodules(ref)
566
+ commit = rev_parse_target(ref)
567
+ return {} unless commit
568
+
569
+ begin
570
+ content = blob_content(commit, ".gitmodules")
571
+ rescue InvalidBlobName
572
+ return {}
573
+ end
574
+
575
+ parse_gitmodules(commit, content)
576
+ end
577
+
578
+ # Return total commits count accessible from passed ref
579
+ def commit_count(ref)
580
+ walker = Rugged::Walker.new(rugged)
581
+ walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
582
+ oid = rugged.rev_parse_oid(ref)
583
+ walker.push(oid)
584
+ walker.count
585
+ end
586
+
587
+ # Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or
588
+ # tag name or a commit SHA. Valid +reset_type+ values are:
589
+ #
590
+ # [:soft]
591
+ # the head will be moved to the commit.
592
+ # [:mixed]
593
+ # will trigger a +:soft+ reset, plus the index will be replaced
594
+ # with the content of the commit tree.
595
+ # [:hard]
596
+ # will trigger a +:mixed+ reset and the working directory will be
597
+ # replaced with the content of the index. (Untracked and ignored files
598
+ # will be left alone)
599
+ delegate :reset, to: :rugged
600
+
601
+ # Mimic the `git clean` command and recursively delete untracked files.
602
+ # Valid keys that can be passed in the +options+ hash are:
603
+ #
604
+ # :d - Remove untracked directories
605
+ # :f - Remove untracked directories that are managed by a different
606
+ # repository
607
+ # :x - Remove ignored files
608
+ #
609
+ # The value in +options+ must evaluate to true for an option to take
610
+ # effect.
611
+ #
612
+ # Examples:
613
+ #
614
+ # repo.clean(d: true, f: true) # Enable the -d and -f options
615
+ #
616
+ # repo.clean(d: false, x: true) # -x is enabled, -d is not
617
+ def clean(options = {})
618
+ strategies = [:remove_untracked]
619
+ strategies.push(:force) if options[:f]
620
+ strategies.push(:remove_ignored) if options[:x]
621
+
622
+ # TODO: implement this method
623
+ end
624
+
625
+ # Check out the specified ref. Valid options are:
626
+ #
627
+ # :b - Create a new branch at +start_point+ and set HEAD to the new
628
+ # branch.
629
+ #
630
+ # * These options are passed to the Rugged::Repository#checkout method:
631
+ #
632
+ # :progress ::
633
+ # A callback that will be executed for checkout progress notifications.
634
+ # Up to 3 parameters are passed on each execution:
635
+ #
636
+ # - The path to the last updated file (or +nil+ on the very first
637
+ # invocation).
638
+ # - The number of completed checkout steps.
639
+ # - The number of total checkout steps to be performed.
640
+ #
641
+ # :notify ::
642
+ # A callback that will be executed for each checkout notification
643
+ # types specified with +:notify_flags+. Up to 5 parameters are passed
644
+ # on each execution:
645
+ #
646
+ # - An array containing the +:notify_flags+ that caused the callback
647
+ # execution.
648
+ # - The path of the current file.
649
+ # - A hash describing the baseline blob (or +nil+ if it does not
650
+ # exist).
651
+ # - A hash describing the target blob (or +nil+ if it does not exist).
652
+ # - A hash describing the workdir blob (or +nil+ if it does not
653
+ # exist).
654
+ #
655
+ # :strategy ::
656
+ # A single symbol or an array of symbols representing the strategies
657
+ # to use when performing the checkout. Possible values are:
658
+ #
659
+ # :none ::
660
+ # Perform a dry run (default).
661
+ #
662
+ # :safe ::
663
+ # Allow safe updates that cannot overwrite uncommitted data.
664
+ #
665
+ # :safe_create ::
666
+ # Allow safe updates plus creation of missing files.
667
+ #
668
+ # :force ::
669
+ # Allow all updates to force working directory to look like index.
670
+ #
671
+ # :allow_conflicts ::
672
+ # Allow checkout to make safe updates even if conflicts are found.
673
+ #
674
+ # :remove_untracked ::
675
+ # Remove untracked files not in index (that are not ignored).
676
+ #
677
+ # :remove_ignored ::
678
+ # Remove ignored files not in index.
679
+ #
680
+ # :update_only ::
681
+ # Only update existing files, don't create new ones.
682
+ #
683
+ # :dont_update_index ::
684
+ # Normally checkout updates index entries as it goes; this stops
685
+ # that.
686
+ #
687
+ # :no_refresh ::
688
+ # Don't refresh index/config/etc before doing checkout.
689
+ #
690
+ # :disable_pathspec_match ::
691
+ # Treat pathspec as simple list of exact match file paths.
692
+ #
693
+ # :skip_locked_directories ::
694
+ # Ignore directories in use, they will be left empty.
695
+ #
696
+ # :skip_unmerged ::
697
+ # Allow checkout to skip unmerged files (NOT IMPLEMENTED).
698
+ #
699
+ # :use_ours ::
700
+ # For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED).
701
+ #
702
+ # :use_theirs ::
703
+ # For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED).
704
+ #
705
+ # :update_submodules ::
706
+ # Recursively checkout submodules with same options (NOT
707
+ # IMPLEMENTED).
708
+ #
709
+ # :update_submodules_if_changed ::
710
+ # Recursively checkout submodules if HEAD moved in super repo (NOT
711
+ # IMPLEMENTED).
712
+ #
713
+ # :disable_filters ::
714
+ # If +true+, filters like CRLF line conversion will be disabled.
715
+ #
716
+ # :dir_mode ::
717
+ # Mode for newly created directories. Default: +0755+.
718
+ #
719
+ # :file_mode ::
720
+ # Mode for newly created files. Default: +0755+ or +0644+.
721
+ #
722
+ # :file_open_flags ::
723
+ # Mode for opening files. Default:
724
+ # <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>.
725
+ #
726
+ # :notify_flags ::
727
+ # A single symbol or an array of symbols representing the cases in
728
+ # which the +:notify+ callback should be invoked. Possible values are:
729
+ #
730
+ # :none ::
731
+ # Do not invoke the +:notify+ callback (default).
732
+ #
733
+ # :conflict ::
734
+ # Invoke the callback for conflicting paths.
735
+ #
736
+ # :dirty ::
737
+ # Invoke the callback for "dirty" files, i.e. those that do not need
738
+ # an update but no longer match the baseline.
739
+ #
740
+ # :updated ::
741
+ # Invoke the callback for any file that was changed.
742
+ #
743
+ # :untracked ::
744
+ # Invoke the callback for untracked files.
745
+ #
746
+ # :ignored ::
747
+ # Invoke the callback for ignored files.
748
+ #
749
+ # :all ::
750
+ # Invoke the callback for all these cases.
751
+ #
752
+ # :paths ::
753
+ # A glob string or an array of glob strings specifying which paths
754
+ # should be taken into account for the checkout operation. +nil+ will
755
+ # match all files. Default: +nil+.
756
+ #
757
+ # :baseline ::
758
+ # A Rugged::Tree that represents the current, expected contents of the
759
+ # workdir. Default: +HEAD+.
760
+ #
761
+ # :target_directory ::
762
+ # A path to an alternative workdir directory in which the checkout
763
+ # should be performed.
764
+ def checkout(ref, options = {}, start_point = "HEAD")
765
+ if options[:b]
766
+ rugged.branches.create(ref, start_point)
767
+ options.delete(:b)
768
+ end
769
+ default_options = { strategy: [:recreate_missing, :safe] }
770
+ rugged.checkout(ref, default_options.merge(options))
771
+ end
772
+
773
+ # Delete the specified branch from the repository
774
+ def delete_branch(branch_name)
775
+ rugged.branches.delete(branch_name)
776
+ end
777
+
778
+ # Create a new branch named **ref+ based on **stat_point+, HEAD by default
779
+ #
780
+ # Examples:
781
+ # create_branch("feature")
782
+ # create_branch("other-feature", "master")
783
+ def create_branch(ref, start_point = "HEAD")
784
+ rugged_ref = rugged.branches.create(ref, start_point)
785
+ Bringit::Branch.new(self, rugged_ref.name, rugged_ref.target)
786
+ rescue Rugged::ReferenceError => e
787
+ raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
788
+ raise InvalidRef.new("Invalid reference #{start_point}")
789
+ end
790
+
791
+ # Return an array of this repository's remote names
792
+ def remote_names
793
+ rugged.remotes.each_name.to_a
794
+ end
795
+
796
+ # Delete the specified remote from this repository.
797
+ def remote_delete(remote_name)
798
+ rugged.remotes.delete(remote_name)
799
+ end
800
+
801
+ # Add a new remote to this repository. Returns a Rugged::Remote object
802
+ def remote_add(remote_name, url)
803
+ rugged.remotes.create(remote_name, url)
804
+ end
805
+
806
+ # Update the specified remote using the values in the +options+ hash
807
+ #
808
+ # Example
809
+ # repo.update_remote("origin", url: "path/to/repo")
810
+ def remote_update(remote_name, options = {})
811
+ # TODO: Implement other remote options
812
+ rugged.remotes.set_url(remote_name, options[:url]) if options[:url]
813
+ end
814
+
815
+ # Fetch the specified remote
816
+ def fetch(remote_name)
817
+ rugged.remotes[remote_name].fetch
818
+ end
819
+
820
+ # Push +*refspecs+ to the remote identified by +remote_name+.
821
+ def push(remote_name, *refspecs)
822
+ rugged.remotes[remote_name].push(refspecs)
823
+ end
824
+
825
+ # Merge the +source_name+ branch into the +target_name+ branch. This is
826
+ # equivalent to `git merge --no_ff +source_name+`, since a merge commit
827
+ # is always created.
828
+ def merge(source_name, target_name, options = {})
829
+ our_commit = rugged.branches[target_name].target
830
+ their_commit = rugged.branches[source_name].target
831
+
832
+ raise "Invalid merge target" if our_commit.nil?
833
+ raise "Invalid merge source" if their_commit.nil?
834
+
835
+ merge_index = rugged.merge_commits(our_commit, their_commit)
836
+ return false if merge_index.conflicts?
837
+
838
+ actual_options = options.merge(
839
+ parents: [our_commit, their_commit],
840
+ tree: merge_index.write_tree(rugged),
841
+ update_ref: "refs/heads/#{target_name}"
842
+ )
843
+ Rugged::Commit.create(rugged, actual_options)
844
+ end
845
+
846
+ def commits_since(from_date)
847
+ walker = Rugged::Walker.new(rugged)
848
+ walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
849
+
850
+ rugged.references.each("refs/heads/*") do |ref|
851
+ walker.push(ref.target_id)
852
+ end
853
+
854
+ commits = []
855
+ walker.each do |commit|
856
+ break if commit.author[:time].to_date < from_date
857
+ commits.push(commit)
858
+ end
859
+
860
+ commits
861
+ end
862
+
863
+ AUTOCRLF_VALUES = {
864
+ "true" => true,
865
+ "false" => false,
866
+ "input" => :input
867
+ }.freeze
868
+
869
+ def autocrlf
870
+ AUTOCRLF_VALUES[rugged.config['core.autocrlf']]
871
+ end
872
+
873
+ def autocrlf=(value)
874
+ rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
875
+ end
876
+
877
+ # Returns result like "git ls-files" , recursive and full file path
878
+ #
879
+ # Ex.
880
+ # repo.ls_files('master')
881
+ #
882
+ def ls_files(ref)
883
+ actual_ref = ref || root_ref
884
+
885
+ begin
886
+ sha_from_ref(actual_ref)
887
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
888
+ # Return an empty array if the ref wasn't found
889
+ return []
890
+ end
891
+
892
+ cmd = %W(git --git-dir=#{path} ls-tree)
893
+ cmd += %w(-r)
894
+ cmd += %w(--full-tree)
895
+ cmd += %w(--full-name)
896
+ cmd += %W(-- #{actual_ref})
897
+
898
+ raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
899
+ stuff, path = f.split("\t")
900
+ _mode, type, _sha = stuff.split(" ")
901
+ path if type == "blob"
902
+ # Contain only blob type
903
+ end
904
+
905
+ raw_output.compact
906
+ end
907
+
908
+ def copy_gitattributes(ref)
909
+ begin
910
+ commit = lookup(ref)
911
+ rescue Rugged::ReferenceError
912
+ raise InvalidRef.new("Ref #{ref} is invalid")
913
+ end
914
+
915
+ # Create the paths
916
+ info_dir_path = File.join(path, 'info')
917
+ info_attributes_path = File.join(info_dir_path, 'attributes')
918
+
919
+ begin
920
+ # Retrieve the contents of the blob
921
+ gitattributes_content = blob_content(commit, '.gitattributes')
922
+ rescue InvalidBlobName
923
+ # No .gitattributes found. Should now remove any info/attributes and return
924
+ File.delete(info_attributes_path) if File.exist?(info_attributes_path)
925
+ return
926
+ end
927
+
928
+ # Create the info directory if needed
929
+ Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path)
930
+
931
+ # Write the contents of the .gitattributes file to info/attributes
932
+ # Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8
933
+ File.open(info_attributes_path, "wb") do |file|
934
+ file.write(gitattributes_content)
935
+ end
936
+ end
937
+
938
+ # Checks if the blob should be diffable according to its attributes
939
+ def diffable?(blob)
940
+ attributes(blob.path).fetch('diff') { blob.text? }
941
+ end
942
+
943
+ # Returns the Git attributes for the given file path.
944
+ #
945
+ # See `Bringit::Attributes` for more information.
946
+ def attributes(path)
947
+ @attributes.attributes(path)
948
+ end
949
+
950
+ private
951
+
952
+ # Get the content of a blob for a given commit. If the blob is a commit
953
+ # (for submodules) then return the blob's OID.
954
+ def blob_content(commit, blob_name)
955
+ blob_entry = tree_entry(commit, blob_name)
956
+
957
+ unless blob_entry
958
+ raise InvalidBlobName.new("Invalid blob name: #{blob_name}")
959
+ end
960
+
961
+ case blob_entry[:type]
962
+ when :commit
963
+ blob_entry[:oid]
964
+ when :tree
965
+ raise InvalidBlobName.new("#{blob_name} is a tree, not a blob")
966
+ when :blob
967
+ rugged.lookup(blob_entry[:oid]).content
968
+ end
969
+ end
970
+
971
+ # Parses the contents of a .gitmodules file and returns a hash of
972
+ # submodule information.
973
+ def parse_gitmodules(commit, content)
974
+ results = {}
975
+
976
+ current = ""
977
+ content.split("\n").each do |txt|
978
+ if txt =~ /^\s*\[/
979
+ current = txt.match(/(?<=").*(?=")/)[0]
980
+ results[current] = {}
981
+ else
982
+ next unless results[current]
983
+ match_data = txt.match(/(\w+)\s*=\s*(.*)/)
984
+ next unless match_data
985
+ target = match_data[2].chomp
986
+ results[current][match_data[1]] = target
987
+
988
+ if match_data[1] == "path"
989
+ begin
990
+ results[current]["id"] = blob_content(commit, target)
991
+ rescue InvalidBlobName
992
+ results.delete(current)
993
+ end
994
+ end
995
+ end
996
+ end
997
+
998
+ results
999
+ end
1000
+
1001
+ # Returns true if +commit+ introduced changes to +path+, using commit
1002
+ # trees to make that determination. Uses the history simplification
1003
+ # rules that `git log` uses by default, where a commit is omitted if it
1004
+ # is TREESAME to any parent.
1005
+ #
1006
+ # If the +follow+ option is true and the file specified by +path+ was
1007
+ # renamed, then the path value is set to the old path.
1008
+ def commit_touches_path?(commit, path, follow, walker)
1009
+ entry = tree_entry(commit, path)
1010
+
1011
+ if commit.parents.empty?
1012
+ # This is the root commit, return true if it has +path+ in its tree
1013
+ return !entry.nil?
1014
+ end
1015
+
1016
+ num_treesame = 0
1017
+ commit.parents.each do |parent|
1018
+ parent_entry = tree_entry(parent, path)
1019
+
1020
+ # Only follow the first TREESAME parent for merge commits
1021
+ if num_treesame > 0
1022
+ walker.hide(parent)
1023
+ next
1024
+ end
1025
+
1026
+ if entry.nil? && parent_entry.nil?
1027
+ num_treesame += 1
1028
+ elsif entry && parent_entry && entry[:oid] == parent_entry[:oid]
1029
+ num_treesame += 1
1030
+ end
1031
+ end
1032
+
1033
+ case num_treesame
1034
+ when 0
1035
+ detect_rename(commit, commit.parents.first, path) if follow
1036
+ true
1037
+ else false
1038
+ end
1039
+ end
1040
+
1041
+ # Find the entry for +path+ in the tree for +commit+
1042
+ def tree_entry(commit, path)
1043
+ pathname = Pathname.new(path)
1044
+ first = true
1045
+ tmp_entry = nil
1046
+
1047
+ pathname.each_filename do |dir|
1048
+ if first
1049
+ tmp_entry = commit.tree[dir]
1050
+ first = false
1051
+ elsif tmp_entry.nil?
1052
+ return nil
1053
+ else
1054
+ tmp_entry = rugged.lookup(tmp_entry[:oid])
1055
+ return nil unless tmp_entry.type == :tree
1056
+ tmp_entry = tmp_entry[dir]
1057
+ end
1058
+ end
1059
+
1060
+ tmp_entry
1061
+ end
1062
+
1063
+ # Compare +commit+ and +parent+ for +path+. If +path+ is a file and was
1064
+ # renamed in +commit+, then set +path+ to the old filename.
1065
+ def detect_rename(commit, parent, path)
1066
+ diff = parent.diff(commit, paths: [path], disable_pathspec_match: true)
1067
+
1068
+ # If +path+ is a filename, not a directory, then we should only have
1069
+ # one delta. We don't need to follow renames for directories.
1070
+ return nil if diff.each_delta.count > 1
1071
+
1072
+ delta = diff.each_delta.first
1073
+ if delta.added?
1074
+ full_diff = parent.diff(commit)
1075
+ full_diff.find_similar!
1076
+
1077
+ full_diff.each_delta do |full_delta|
1078
+ if full_delta.renamed? && path == full_delta.new_file[:path]
1079
+ # Look for the old path in ancestors
1080
+ path.replace(full_delta.old_file[:path])
1081
+ end
1082
+ end
1083
+ end
1084
+ end
1085
+
1086
+ def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n))
1087
+ git_archive_cmd = %W(git --git-dir=#{path} archive)
1088
+
1089
+ # Put files into a directory before archiving
1090
+ prefix = "#{archive_name(treeish)}/"
1091
+ git_archive_cmd << "--prefix=#{prefix}"
1092
+
1093
+ # Format defaults to tar
1094
+ git_archive_cmd << "--format=#{format}" if format
1095
+
1096
+ git_archive_cmd += %W(-- #{treeish})
1097
+
1098
+ open(filename, 'w') do |file|
1099
+ # Create a pipe to act as the '|' in 'git archive ... | gzip'
1100
+ pipe_rd, pipe_wr = IO.pipe
1101
+
1102
+ # Get the compression process ready to accept data from the read end
1103
+ # of the pipe
1104
+ compress_pid = spawn(*nice(compress_cmd), in: pipe_rd, out: file)
1105
+ # The read end belongs to the compression process now; we should
1106
+ # close our file descriptor for it.
1107
+ pipe_rd.close
1108
+
1109
+ # Start 'git archive' and tell it to write into the write end of the
1110
+ # pipe.
1111
+ git_archive_pid = spawn(*nice(git_archive_cmd), out: pipe_wr)
1112
+ # The write end belongs to 'git archive' now; close it.
1113
+ pipe_wr.close
1114
+
1115
+ # When 'git archive' and the compression process are finished, we are
1116
+ # done.
1117
+ Process.waitpid(git_archive_pid)
1118
+ raise "#{git_archive_cmd.join(' ')} failed" unless $?.success?
1119
+ Process.waitpid(compress_pid)
1120
+ raise "#{compress_cmd.join(' ')} failed" unless $?.success?
1121
+ end
1122
+ end
1123
+
1124
+ def nice(cmd)
1125
+ nice_cmd = %w(nice -n 20)
1126
+ unless unsupported_platform?
1127
+ nice_cmd += %w(ionice -c 2 -n 7)
1128
+ end
1129
+ nice_cmd + cmd
1130
+ end
1131
+
1132
+ def unsupported_platform?
1133
+ %w[darwin freebsd solaris].map { |platform| RUBY_PLATFORM.include?(platform) }.any?
1134
+ end
1135
+
1136
+ # Returns true if the index entry has the special file mode that denotes
1137
+ # a submodule.
1138
+ def submodule?(index_entry)
1139
+ index_entry[:mode] == 57344
1140
+ end
1141
+
1142
+ # Return a Rugged::Index that has read from the tree at +ref_name+
1143
+ def populated_index(ref_name)
1144
+ commit = rev_parse_target(ref_name)
1145
+ index = rugged.index
1146
+ index.read_tree(commit.tree)
1147
+ index
1148
+ end
1149
+
1150
+ # Return an array of BlobSnippets for lines in +file_contents+ that match
1151
+ # +query+
1152
+ def build_greps(file_contents, query, ref, filename)
1153
+ # The file_contents string is potentially huge so we make sure to loop
1154
+ # through it one line at a time. This gives Ruby the chance to GC lines
1155
+ # we are not interested in.
1156
+ #
1157
+ # We need to do a little extra work because we are not looking for just
1158
+ # the lines that matches the query, but also for the context
1159
+ # (surrounding lines). We will use Enumerable#each_cons to efficiently
1160
+ # loop through the lines while keeping surrounding lines on hand.
1161
+ #
1162
+ # First, we turn "foo\nbar\nbaz" into
1163
+ # [
1164
+ # [nil, -3], [nil, -2], [nil, -1],
1165
+ # ['foo', 0], ['bar', 1], ['baz', 3],
1166
+ # [nil, 4], [nil, 5], [nil, 6]
1167
+ # ]
1168
+ lines_with_index = Enumerator.new do |yielder|
1169
+ # Yield fake 'before' lines for the first line of file_contents
1170
+ (-SEARCH_CONTEXT_LINES..-1).each do |i|
1171
+ yielder.yield [nil, i]
1172
+ end
1173
+
1174
+ # Yield the actual file contents
1175
+ count = 0
1176
+ file_contents.each_line do |line|
1177
+ line.chomp!
1178
+ yielder.yield [line, count]
1179
+ count += 1
1180
+ end
1181
+
1182
+ # Yield fake 'after' lines for the last line of file_contents
1183
+ (count + 1..count + SEARCH_CONTEXT_LINES).each do |i|
1184
+ yielder.yield [nil, i]
1185
+ end
1186
+ end
1187
+
1188
+ greps = []
1189
+
1190
+ # Loop through consecutive blocks of lines with indexes
1191
+ lines_with_index.each_cons(2 * SEARCH_CONTEXT_LINES + 1) do |line_block|
1192
+ # Get the 'middle' line and index from the block
1193
+ line, _ = line_block[SEARCH_CONTEXT_LINES]
1194
+
1195
+ next unless line && line.match(/#{Regexp.escape(query)}/i)
1196
+
1197
+ # Yay, 'line' contains a match!
1198
+ # Get an array with just the context lines (no indexes)
1199
+ match_with_context = line_block.map(&:first)
1200
+ # Remove 'nil' lines in case we are close to the first or last line
1201
+ match_with_context.compact!
1202
+
1203
+ # Get the line number (1-indexed) of the first context line
1204
+ first_context_line_number = line_block[0][1] + 1
1205
+
1206
+ greps << Bringit::BlobSnippet.new(
1207
+ ref,
1208
+ match_with_context,
1209
+ first_context_line_number,
1210
+ filename
1211
+ )
1212
+ end
1213
+
1214
+ greps
1215
+ end
1216
+
1217
+ # Return the Rugged patches for the diff between +from+ and +to+.
1218
+ def diff_patches(from, to, options = {}, *paths)
1219
+ options ||= {}
1220
+ paths = paths.map { |p| p.sub(%r{\A/}, '') }
1221
+ break_rewrites = options[:break_rewrites]
1222
+ actual_options = Bringit::Diff.filter_diff_options(options.merge(paths: paths))
1223
+
1224
+ diff = rugged.diff(from, to, actual_options)
1225
+ return [] if diff.nil?
1226
+ diff.find_similar!(break_rewrites: break_rewrites)
1227
+ diff.each_patch
1228
+ end
1229
+ end
1230
+ end