bringit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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