gitlab_git 6.3.0 → 7.0.0.rc1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f512e6aa7aeac3ab0fd43175bc3d53e50f510776
4
- data.tar.gz: 4949e60987b1dbe296a97d0a041a09091cd1cf44
3
+ metadata.gz: badf0b7ecbe40208622e3e555055ab825f98ad6e
4
+ data.tar.gz: f85a1a6a30411397118fe910b8f0479799ddfcf2
5
5
  SHA512:
6
- metadata.gz: 9b4d0e7beb1942463241b37cee1e02c719a0a8cbfca5f5d5a1920d91b47ab4f2be781dd0f33e33ff312b31154162cc3219e465375ee6c56fa0242b5b32980eaf
7
- data.tar.gz: a370d903817cfc3f8893741ddd042cee6d74482cfcfaed36d35e257223bf7efa846104c2fd5da75a84da2a737140b482debed0b79a4f76a34eb44f1339d44a08
6
+ metadata.gz: 4f12a58cdb7e060e72096836c8cff6ab7ca1f56870c69b09cb9af578198e27c1a39234e3c21cff566a84569dd8d02e93a89e3bb6b475443f83ab6c007bc77e49
7
+ data.tar.gz: 5cc43362edf2f18cb84f6090a46e380153d5ac5b44c94e440b556c9b55ac0a0c39d8c84f11f734840d0a6559ccbab22ccdf88e17d315f627201014ad9a7489a8
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.0
1
+ 7.0.0.rc1
data/lib/gitlab_git.rb CHANGED
@@ -1,18 +1,12 @@
1
1
  # Libraries
2
2
  require 'ostruct'
3
3
  require 'fileutils'
4
- require 'grit'
5
4
  require 'linguist'
6
5
  require 'active_support/core_ext/hash/keys'
7
6
  require 'active_support/core_ext/object/try'
8
- require 'grit'
9
- require 'grit_ext'
10
7
  require 'rugged'
11
8
  require "charlock_holmes"
12
-
13
- Grit::Blob.class_eval do
14
- include Linguist::BlobHelper
15
- end
9
+ require "zip"
16
10
 
17
11
  # Gitlab::Git
18
12
  require_relative "gitlab_git/popen"
@@ -20,6 +14,7 @@ require_relative 'gitlab_git/encoding_helper'
20
14
  require_relative "gitlab_git/blame"
21
15
  require_relative "gitlab_git/blob"
22
16
  require_relative "gitlab_git/commit"
17
+ require_relative "gitlab_git/commit_stats"
23
18
  require_relative "gitlab_git/compare"
24
19
  require_relative "gitlab_git/diff"
25
20
  require_relative "gitlab_git/repository"
@@ -13,7 +13,7 @@ module Gitlab
13
13
  end
14
14
 
15
15
  def data
16
- lines.join("\n")
16
+ lines.join("\n") if lines
17
17
  end
18
18
 
19
19
  def name
@@ -1,6 +1,4 @@
1
- # Gitlab::Git::Commit is a wrapper around native Grit::Commit object
2
- # We dont want to use grit objects inside app/
3
- # It helps us easily migrate to rugged in future
1
+ # Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
4
2
  module Gitlab
5
3
  module Git
6
4
  class Commit
@@ -13,6 +11,18 @@ module Gitlab
13
11
  ]
14
12
  attr_accessor *SERIALIZE_KEYS
15
13
 
14
+ def ==(other)
15
+ return false unless other.is_a?(Gitlab::Git::Commit)
16
+
17
+ methods = [:message, :parent_ids, :authored_date, :author_name,
18
+ :author_email, :committed_date, :committer_name,
19
+ :committer_email]
20
+
21
+ methods.all? do |method|
22
+ send(method) == other.send(method)
23
+ end
24
+ end
25
+
16
26
  class << self
17
27
  # Get commits collection
18
28
  #
@@ -39,9 +49,17 @@ module Gitlab
39
49
  #
40
50
  # Commit.find(repo, 'master')
41
51
  #
42
- def find(repo, commit_id = nil)
43
- commit = repo.log(ref: commit_id, limit: 1).first
52
+ def find(repo, commit_id = "HEAD")
53
+ return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
54
+
55
+ obj = repo.rugged.rev_parse(commit_id)
56
+ commit = case obj
57
+ when Rugged::Tag::Annotation then obj.target
58
+ when Rugged::Commit then obj
59
+ end
44
60
  decorate(commit) if commit
61
+ rescue Rugged::ReferenceError, Rugged::ObjectError
62
+ nil
45
63
  end
46
64
 
47
65
  # Get last commit for HEAD
@@ -50,7 +68,7 @@ module Gitlab
50
68
  # Commit.last(repo)
51
69
  #
52
70
  def last(repo)
53
- find(repo, nil)
71
+ find(repo)
54
72
  end
55
73
 
56
74
  # Get last commit for specified path and ref
@@ -78,6 +96,8 @@ module Gitlab
78
96
  repo.commits_between(base, head).map do |commit|
79
97
  decorate(commit)
80
98
  end
99
+ rescue Rugged::ReferenceError
100
+ []
81
101
  end
82
102
 
83
103
  # Delegate Repository#find_commits
@@ -98,7 +118,7 @@ module Gitlab
98
118
  elsif raw_commit.is_a?(Rugged::Commit)
99
119
  init_from_rugged(raw_commit)
100
120
  else
101
- init_from_grit(raw_commit)
121
+ raise "Invalid raw commit type: #{raw_commit.class}"
102
122
  end
103
123
 
104
124
  @head = head
@@ -133,7 +153,6 @@ module Gitlab
133
153
  #
134
154
  # Cuts out the header and stats from #to_patch and returns only the diff.
135
155
  def to_diff
136
- # see Grit::Commit#show
137
156
  patch = to_patch
138
157
 
139
158
  # discard lines before the diff
@@ -146,6 +165,17 @@ module Gitlab
146
165
  lines.join("\n")
147
166
  end
148
167
 
168
+ # Returns a diff object for the changes from this commit's first parent.
169
+ # If there is no parent, then the diff is between this commit and an
170
+ # empty repo.
171
+ def diff_from_parent
172
+ if raw_commit.parents.empty?
173
+ raw_commit.diff(reverse: true)
174
+ else
175
+ raw_commit.parents[0].diff(raw_commit)
176
+ end
177
+ end
178
+
149
179
  def has_zero_stats?
150
180
  stats.total.zero?
151
181
  rescue
@@ -167,11 +197,11 @@ module Gitlab
167
197
  end
168
198
 
169
199
  def diffs
170
- raw_commit.diffs.map { |diff| Gitlab::Git::Diff.new(diff) }
200
+ diff_from_parent.map { |diff| Gitlab::Git::Diff.new(diff) }
171
201
  end
172
202
 
173
203
  def parents
174
- raw_commit.parents
204
+ raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
175
205
  end
176
206
 
177
207
  def tree
@@ -179,14 +209,14 @@ module Gitlab
179
209
  end
180
210
 
181
211
  def stats
182
- raw_commit.stats
212
+ Gitlab::Git::CommitStats.new(self)
183
213
  end
184
214
 
185
215
  def to_patch
186
- raw_commit.to_patch
216
+ raw_commit.to_mbox
187
217
  end
188
218
 
189
- # Get refs collection(Grit::Head or Grit::Remote or Grit::Tag)
219
+ # Get a collection of Rugged::Reference objects for this commit.
190
220
  #
191
221
  # Ex.
192
222
  # commit.ref(repo)
@@ -201,24 +231,13 @@ module Gitlab
201
231
  # commit.ref_names(repo)
202
232
  #
203
233
  def ref_names(repo)
204
- refs(repo).map(&:name)
234
+ refs(repo).map do |ref|
235
+ ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
236
+ end
205
237
  end
206
238
 
207
239
  private
208
240
 
209
- def init_from_grit(grit)
210
- @raw_commit = grit
211
- @id = grit.id
212
- @message = grit.message
213
- @authored_date = grit.authored_date
214
- @committed_date = grit.committed_date
215
- @author_name = grit.author.name
216
- @author_email = grit.author.email
217
- @committer_name = grit.committer.name
218
- @committer_email = grit.committer.email
219
- @parent_ids = grit.parents.map(&:id)
220
- end
221
-
222
241
  def init_from_hash(hash)
223
242
  raw_commit = hash.symbolize_keys
224
243
 
@@ -0,0 +1,26 @@
1
+ # Gitlab::Git::CommitStats counts the additions, deletions, and total changes
2
+ # in a commit.
3
+ module Gitlab
4
+ module Git
5
+ class CommitStats
6
+ attr_reader :id, :additions, :deletions, :total
7
+
8
+ # Instantiate a CommitStats object
9
+ def initialize(commit)
10
+ @id = commit.id
11
+ @additions = 0
12
+ @deletions = 0
13
+ @total = 0
14
+
15
+ diff = commit.diff_from_parent
16
+
17
+ diff.each_patch do |p|
18
+ # TODO: Use the new Rugged convenience methods when they're released
19
+ @additions += p.stat[0]
20
+ @deletions += p.stat[1]
21
+ @total += p.changes
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,10 +1,9 @@
1
- # Gitlab::Git::Diff is a wrapper around native Grit::Diff object
2
- # We dont want to use grit objects inside app/
3
- # It helps us easily migrate to rugged in future
1
+ # Gitlab::Git::Diff is a wrapper around native Rugged::Diff object
4
2
  module Gitlab
5
3
  module Git
6
4
  class Diff
7
5
  class TimeoutError < StandardError; end
6
+ include EncodingHelper
8
7
 
9
8
  attr_accessor :raw_diff
10
9
 
@@ -21,11 +20,7 @@ module Gitlab
21
20
  # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B"
22
21
  common_commit = repo.merge_base_commit(head, base)
23
22
 
24
- repo.diff(common_commit, head, *paths).map do |diff|
25
- Gitlab::Git::Diff.new(diff)
26
- end
27
- rescue Grit::Git::GitTimeout
28
- raise TimeoutError.new("Diff.between exited with timeout")
23
+ repo.diff(common_commit, head, *paths)
29
24
  end
30
25
  end
31
26
 
@@ -34,8 +29,10 @@ module Gitlab
34
29
 
35
30
  if raw_diff.is_a?(Hash)
36
31
  init_from_hash(raw_diff)
32
+ elsif raw_diff.is_a?(Rugged::Patch)
33
+ init_from_rugged(raw_diff)
37
34
  else
38
- init_from_grit(raw_diff)
35
+ raise "Invalid raw diff type: #{raw_diff.class}"
39
36
  end
40
37
  end
41
38
 
@@ -57,12 +54,19 @@ module Gitlab
57
54
 
58
55
  private
59
56
 
60
- def init_from_grit(grit)
61
- @raw_diff = grit
57
+ def init_from_rugged(rugged)
58
+ @raw_diff = rugged
62
59
 
63
- serialize_keys.each do |key|
64
- send(:"#{key}=", grit.send(key))
65
- end
60
+ @diff = encode!(strip_diff_headers(rugged.to_s))
61
+
62
+ d = rugged.delta
63
+ @new_path = encode!(d.new_file[:path])
64
+ @old_path = encode!(d.old_file[:path])
65
+ @a_mode = d.old_file[:mode].to_s(8)
66
+ @b_mode = d.new_file[:mode].to_s(8)
67
+ @new_file = d.added?
68
+ @renamed_file = d.renamed?
69
+ @deleted_file = d.deleted?
66
70
  end
67
71
 
68
72
  def init_from_hash(hash)
@@ -72,6 +76,14 @@ module Gitlab
72
76
  send(:"#{key}=", raw_diff[key.to_sym])
73
77
  end
74
78
  end
79
+
80
+ # Strip out the information at the beginning of the patch's text to match
81
+ # Grit's output
82
+ def strip_diff_headers(diff_text)
83
+ lines = diff_text.split("\n")
84
+ lines.shift until lines.empty? || lines.first.match("^(---|Binary)")
85
+ lines.join("\n")
86
+ end
75
87
  end
76
88
  end
77
89
  end
@@ -9,22 +9,67 @@ module Gitlab
9
9
  @repo, @ref, @timeout = repo, ref, timeout
10
10
  end
11
11
 
12
+ # Returns a string of log information; equivalent to running 'git log`
13
+ # with these options:
14
+ #
15
+ # -6000
16
+ # --format=%aN%x0a%aE%x0a%cd
17
+ # --date=short
18
+ # --shortstat
19
+ # --no-merges
20
+ # --diff-filter=ACDM
12
21
  def log
13
- log = nil
14
- Grit::Git.with_timeout(timeout) do
15
- # Limit log to 6k commits to avoid timeout for huge projects
16
- args = [ref, '-6000', '--format=%aN%x0a%aE%x0a%cd', '--date=short', '--shortstat', '--no-merges', '--diff-filter=ACDM']
17
- log = repo.git.native(:log, {}, args)
22
+ commit_strings = []
23
+ walker = Rugged::Walker.new(repo.rugged)
24
+ walker.push(repo.lookup(ref))
25
+ walker.each(limit: 6000) do |commit|
26
+ # Skip merge commits
27
+ next if commit.parents.length > 1
28
+
29
+ g_commit = Gitlab::Git::Commit.new(commit)
30
+
31
+ commit_strings << [
32
+ g_commit.author_name,
33
+ g_commit.author_email,
34
+ g_commit.committed_date.strftime("%Y-%m-%d"),
35
+ "",
36
+ status_string(g_commit)
37
+ ].join("\n")
18
38
  end
19
39
 
20
- log
21
- rescue Grit::Git::GitTimeout
22
- nil
40
+ commit_strings.join("\n")
23
41
  end
24
42
 
25
43
  def parsed_log
26
44
  LogParser.parse_log(log)
27
45
  end
46
+
47
+ private
48
+
49
+ # Returns a string describing the files changed, additions and deletions
50
+ # for +commit+
51
+ def status_string(commit)
52
+ stats = commit.stats
53
+
54
+ status = "#{num_files_changed(commit)} files changed"
55
+ status << ", #{stats.additions} insertions" if stats.additions > 0
56
+ status << ", #{stats.deletions} deletions" if stats.deletions > 0
57
+ status
58
+ end
59
+
60
+ # Returns the number of files that were either added, copied, deleted, or
61
+ # modified by +commit+
62
+ def num_files_changed(commit)
63
+ count = 0
64
+
65
+ diff = commit.diff_from_parent
66
+ diff.find_similar!
67
+ diff.each_delta do |d|
68
+ count += 1 if d.added? || d.copied? || d.deleted? || d.modified?
69
+ end
70
+
71
+ count
72
+ end
28
73
  end
29
74
  end
30
75
  end
@@ -1,8 +1,7 @@
1
- # Gitlab::Git::Repository is a wrapper around native Grit::Repository object
2
- # We dont want to use grit objects inside app/
3
- # It helps us easily migrate to rugged in future
1
+ # Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
4
2
  require_relative 'encoding_helper'
5
3
  require 'tempfile'
4
+ require "rubygems/package"
6
5
 
7
6
  module Gitlab
8
7
  module Git
@@ -20,9 +19,6 @@ module Gitlab
20
19
  # Directory name of repo
21
20
  attr_reader :name
22
21
 
23
- # Grit repo object
24
- attr_reader :grit
25
-
26
22
  # Rugged repo object
27
23
  attr_reader :rugged
28
24
 
@@ -32,15 +28,9 @@ module Gitlab
32
28
  @root_ref = discover_default_branch
33
29
  end
34
30
 
35
- def grit
36
- @grit ||= Grit::Repo.new(path)
37
- rescue Grit::NoSuchPathError
38
- raise NoRepository.new('no repository for such path')
39
- end
40
-
41
31
  # Alias to old method for compatibility
42
32
  def raw
43
- grit
33
+ rugged
44
34
  end
45
35
 
46
36
  def rugged
@@ -69,18 +59,8 @@ module Gitlab
69
59
 
70
60
  # Returns an Array of Tags
71
61
  def tags
72
- rugged.refs.select do |ref|
73
- ref.name =~ /\Arefs\/tags/
74
- end.map do |rugged_ref|
75
- target = rugged_ref.target
76
- message = nil
77
- if rugged_ref.target.is_a?(Rugged::Tag::Annotation) &&
78
- rugged_ref.target.target.is_a?(Rugged::Commit)
79
- unless rugged_ref.target.target.message == rugged_ref.target.message
80
- message = rugged_ref.target.message.chomp
81
- end
82
- end
83
- Tag.new(rugged_ref.name, target, message)
62
+ rugged.references.each("refs/tags/*").map do |ref|
63
+ Tag.new(ref.name, ref.target, ref.target.message.chomp)
84
64
  end.sort_by(&:name)
85
65
  end
86
66
 
@@ -91,7 +71,9 @@ module Gitlab
91
71
 
92
72
  # Deprecated. Will be removed in 5.2
93
73
  def heads
94
- @heads ||= grit.heads.sort_by(&:name)
74
+ rugged.references.each("refs/heads/*").map do |head|
75
+ Gitlab::Git::Ref.new(head.name, head.target)
76
+ end.sort_by(&:name)
95
77
  end
96
78
 
97
79
  def has_commits?
@@ -137,12 +119,11 @@ module Gitlab
137
119
  # app_root/tmp/repositories/project_name/project_name-commit-id.tag.gz
138
120
  #
139
121
  def archive_repo(ref, storage_path, format = "tar.gz")
140
- ref = ref || self.root_ref
122
+ ref ||= root_ref
141
123
  commit = Gitlab::Git::Commit.find(self, ref)
142
124
  return nil unless commit
143
125
 
144
126
  extension = nil
145
- git_archive_format = nil
146
127
  pipe_cmd = nil
147
128
 
148
129
  case format
@@ -154,12 +135,10 @@ module Gitlab
154
135
  pipe_cmd = %W(cat)
155
136
  when "zip"
156
137
  extension = ".zip"
157
- git_archive_format = "zip"
158
138
  pipe_cmd = %W(cat)
159
139
  else
160
140
  # everything else should fall back to tar.gz
161
141
  extension = ".tar.gz"
162
- git_archive_format = nil
163
142
  pipe_cmd = %W(gzip -n)
164
143
  end
165
144
 
@@ -167,23 +146,8 @@ module Gitlab
167
146
  file_name = self.name.gsub("\.git", "") + "-" + commit.id.to_s + extension
168
147
  file_path = File.join(storage_path, self.name, file_name)
169
148
 
170
- # Put files into a directory before archiving
171
- prefix = File.basename(self.name) + "/"
172
-
173
149
  # Create file if not exists
174
- unless File.exists?(file_path)
175
- # create archive in temp file
176
- tmp_file = Tempfile.new('gitlab-archive-repo', storage_path)
177
- self.grit.archive_to_file(ref, prefix, tmp_file.path, git_archive_format, pipe_cmd)
178
-
179
- # move temp file to persisted location
180
- FileUtils.mkdir_p File.dirname(file_path)
181
- FileUtils.move(tmp_file.path, file_path)
182
-
183
- # delte temp file
184
- tmp_file.close
185
- tmp_file.unlink
186
- end
150
+ create_archive(ref, pipe_cmd, file_path) unless File.exist?(file_path)
187
151
 
188
152
  file_path
189
153
  end
@@ -194,19 +158,24 @@ module Gitlab
194
158
  (size.to_f / 1024).round(2)
195
159
  end
196
160
 
161
+ # Returns an array of BlobSnippets for files at the specified +ref+ that
162
+ # contain the +query+ string.
197
163
  def search_files(query, ref = nil)
198
- if ref.nil? || ref == ""
199
- ref = root_ref
200
- end
164
+ greps = []
165
+ ref ||= root_ref
201
166
 
202
- greps = grit.grep(query, 3, ref)
167
+ populated_index(ref).each do |entry|
168
+ # Discard submodules
169
+ next if submodule?(entry)
203
170
 
204
- greps.map do |grep|
205
- Gitlab::Git::BlobSnippet.new(ref, grep.content, grep.startline, grep.filename)
171
+ content = rugged.lookup(entry[:oid]).content
172
+ greps += build_greps(content, query, ref, entry[:path])
206
173
  end
174
+
175
+ greps
207
176
  end
208
177
 
209
- # Delegate log to Grit method
178
+ # Use the Rugged Walker API to build an array of commits.
210
179
  #
211
180
  # Usage.
212
181
  # repo.log(
@@ -226,28 +195,61 @@ module Gitlab
226
195
  }
227
196
 
228
197
  options = default_options.merge(options)
198
+ options[:limit] ||= 0
199
+ options[:offset] ||= 0
200
+ actual_ref = options[:ref] || root_ref
201
+ sha = sha_from_ref(actual_ref)
202
+
203
+ build_log(sha, options)
204
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
205
+ # Return an empty array if the ref wasn't found
206
+ []
207
+ end
229
208
 
230
- grit.log(
231
- options[:ref] || root_ref,
232
- options[:path],
233
- max_count: options[:limit].to_i,
234
- skip: options[:offset].to_i,
235
- follow: options[:follow]
236
- )
209
+ def sha_from_ref(ref)
210
+ sha = rugged.rev_parse_oid(ref)
211
+ object = rugged.lookup(sha)
212
+
213
+ if object.kind_of?(Rugged::Commit)
214
+ sha
215
+ elsif object.respond_to?(:target)
216
+ sha_from_ref(object.target.oid)
217
+ end
237
218
  end
238
219
 
239
- # Delegate commits_between to Grit method
220
+ # Return a collection of Rugged::Commits between the two SHA arguments.
240
221
  #
241
222
  def commits_between(from, to)
242
- grit.commits_between(from, to)
223
+ walker = Rugged::Walker.new(rugged)
224
+ walker.push(to)
225
+ walker.hide(from)
226
+ commits = walker.to_a
227
+ walker.reset
228
+
229
+ commits.reverse
243
230
  end
244
231
 
232
+ # Returns the SHA of the most recent common ancestor of +from+ and +to+
245
233
  def merge_base_commit(from, to)
246
- grit.git.native(:merge_base, {}, [to, from]).strip
234
+ rugged.merge_base(from, to)
247
235
  end
248
236
 
237
+ # Return an array of Diff objects that represent the diff
238
+ # between +from+ and +to+.
249
239
  def diff(from, to, *paths)
250
- grit.diff(from, to, *paths)
240
+ rugged.diff(from, to, paths: paths).patches.map do |p|
241
+ Gitlab::Git::Diff.new(p)
242
+ end
243
+ end
244
+
245
+ # Return the diff between +from+ and +to+ in a single patch string.
246
+ def diff_text(from, to, *paths)
247
+ # NOTE: It would be simpler to use the Rugged::Diff#patch method, but
248
+ # that formats the diff text differently than Rugged::Patch#to_s for
249
+ # changes to binary files.
250
+ rugged.diff(from, to, paths: paths).patches.map do |p|
251
+ p.to_s
252
+ end.join("\n")
251
253
  end
252
254
 
253
255
  # Returns commits collection
@@ -272,81 +274,92 @@ module Gitlab
272
274
 
273
275
  allowed_options = [:ref, :max_count, :skip, :contains, :order]
274
276
 
275
- actual_options.keep_if do |key, value|
277
+ actual_options.keep_if do |key|
276
278
  allowed_options.include?(key)
277
279
  end
278
280
 
279
- default_options = {pretty: 'raw', order: :date}
280
-
281
+ default_options = { skip: 0 }
281
282
  actual_options = default_options.merge(actual_options)
282
283
 
283
- order = actual_options.delete(:order)
284
+ walker = Rugged::Walker.new(rugged)
284
285
 
285
- case order
286
- when :date
287
- actual_options[:date_order] = true
288
- when :topo
289
- actual_options[:topo_order] = true
286
+ if actual_options[:ref]
287
+ walker.push(rugged.rev_parse_oid(actual_options[:ref]))
288
+ elsif actual_options[:contains]
289
+ branches_contains(actual_options[:contains]).each do |branch|
290
+ walker.push(branch.target_id)
291
+ end
292
+ else
293
+ rugged.references.each("refs/heads/*") do |ref|
294
+ walker.push(ref.target_id)
295
+ end
290
296
  end
291
297
 
292
- ref = actual_options.delete(:ref)
293
-
294
- containing_commit = actual_options.delete(:contains)
298
+ walker.sorting(Rugged::SORT_TOPO) if actual_options[:order] == :topo
295
299
 
296
- args = []
297
-
298
- if ref
299
- args.push(ref)
300
- elsif containing_commit
301
- args.push(*branch_names_contains(containing_commit))
302
- else
303
- actual_options[:all] = true
300
+ commits = []
301
+ offset = actual_options[:skip]
302
+ limit = actual_options[:max_count]
303
+ walker.each(offset: offset, limit: limit) do |commit|
304
+ gitlab_commit = Gitlab::Git::Commit.decorate(commit)
305
+ commits.push(gitlab_commit)
304
306
  end
305
307
 
306
- output = grit.git.native(:rev_list, actual_options, *args)
308
+ walker.reset
307
309
 
308
- Grit::Commit.list_from_string(grit, output).map do |commit|
309
- Gitlab::Git::Commit.decorate(commit)
310
- end
311
- rescue Grit::GitRuby::Repository::NoSuchShaFound
310
+ commits
311
+ rescue Rugged::OdbError
312
312
  []
313
313
  end
314
314
 
315
- # Returns branch names collection that contains the special commit(SHA1 or name)
315
+ # Returns branch names collection that contains the special commit(SHA1
316
+ # or name)
316
317
  #
317
318
  # Ex.
318
319
  # repo.branch_names_contains('master')
319
320
  #
320
321
  def branch_names_contains(commit)
321
- output = grit.git.native(:branch, {contains: true}, commit)
322
+ branches_contains(commit).map { |c| c.target_id }
323
+ end
322
324
 
323
- # Fix encoding issue
324
- output = EncodingHelper::encode!(output)
325
+ # Returns branch collection that contains the special commit(SHA1 or name)
326
+ #
327
+ # Ex.
328
+ # repo.branch_names_contains('master')
329
+ #
330
+ def branches_contains(commit)
331
+ sha = rugged.rev_parse_oid(commit)
332
+
333
+ walker = Rugged::Walker.new(rugged)
325
334
 
326
- # The output is expected as follow
327
- # fix-aaa
328
- # fix-bbb
329
- # * master
330
- output.scan(/[^* \n]+/)
335
+ rugged.branches.select do |branch|
336
+ walker.push(branch.target_id)
337
+ result = walker.any? { |c| c.oid == sha }
338
+ walker.reset
339
+
340
+ result
341
+ end
331
342
  end
332
343
 
333
344
  # Get refs hash which key is SHA1
334
- # and value is ref object(Grit::Head or Grit::Remote or Grit::Tag)
345
+ # and value is a Rugged::Reference
335
346
  def refs_hash
336
347
  # Initialize only when first call
337
348
  if @refs_hash.nil?
338
349
  @refs_hash = Hash.new { |h, k| h[k] = [] }
339
350
 
340
- grit.refs.each do |r|
341
- @refs_hash[r.commit.id] << r
351
+ rugged.references.each do |r|
352
+ sha = rev_parse_target(r.target.oid).oid
353
+
354
+ @refs_hash[sha] << r
342
355
  end
343
356
  end
344
357
  @refs_hash
345
358
  end
346
359
 
347
- # Lookup for rugged object by oid
348
- def lookup(oid)
349
- rugged.lookup(oid)
360
+ # Lookup for rugged object by oid or ref name
361
+ def lookup(oid_or_ref_name)
362
+ rugged.rev_parse(oid_or_ref_name)
350
363
  end
351
364
 
352
365
  # Return hash with submodules info for this repository
@@ -364,7 +377,10 @@ module Gitlab
364
377
  # }
365
378
  #
366
379
  def submodules(ref)
367
- Grit::Submodule.config(grit, ref)
380
+ tree = rugged.lookup(rugged.rev_parse_oid(ref)).tree
381
+
382
+ content = blob_content(tree, ".gitmodules")
383
+ parse_gitmodules(tree, content)
368
384
  end
369
385
 
370
386
  # Return total commits count accessible from passed ref
@@ -374,6 +390,532 @@ module Gitlab
374
390
  walker.push(ref)
375
391
  walker.count
376
392
  end
393
+
394
+ # Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or
395
+ # tag name or a commit SHA. Valid +reset_type+ values are:
396
+ #
397
+ # [:soft]
398
+ # the head will be moved to the commit.
399
+ # [:mixed]
400
+ # will trigger a +:soft+ reset, plus the index will be replaced
401
+ # with the content of the commit tree.
402
+ # [:hard]
403
+ # will trigger a +:mixed+ reset and the working directory will be
404
+ # replaced with the content of the index. (Untracked and ignored files
405
+ # will be left alone)
406
+ def reset(ref, reset_type)
407
+ rugged.reset(ref, reset_type)
408
+ end
409
+
410
+ # Mimic the `git clean` command and recursively delete untracked files.
411
+ # Valid keys that can be passed in the +options+ hash are:
412
+ #
413
+ # :d - Remove untracked directories
414
+ # :f - Remove untracked directories that are managed by a different
415
+ # repository
416
+ # :x - Remove ignored files
417
+ #
418
+ # The value in +options+ must evaluate to true for an option to take
419
+ # effect.
420
+ #
421
+ # Examples:
422
+ #
423
+ # repo.clean(d: true, f: true) # Enable the -d and -f options
424
+ #
425
+ # repo.clean(d: false, x: true) # -x is enabled, -d is not
426
+ def clean(options = {})
427
+ strategies = [:remove_untracked]
428
+ strategies.push(:force) if options[:f]
429
+ strategies.push(:remove_ignored) if options[:x]
430
+
431
+ # TODO: implement this method
432
+ end
433
+
434
+ # Check out the specified ref. Valid options are:
435
+ #
436
+ # :b - Create a new branch at +start_point+ and set HEAD to the new
437
+ # branch.
438
+ #
439
+ # * These options are passed to the Rugged::Repository#checkout method:
440
+ #
441
+ # :progress ::
442
+ # A callback that will be executed for checkout progress notifications.
443
+ # Up to 3 parameters are passed on each execution:
444
+ #
445
+ # - The path to the last updated file (or +nil+ on the very first
446
+ # invocation).
447
+ # - The number of completed checkout steps.
448
+ # - The number of total checkout steps to be performed.
449
+ #
450
+ # :notify ::
451
+ # A callback that will be executed for each checkout notification
452
+ # types specified with +:notify_flags+. Up to 5 parameters are passed
453
+ # on each execution:
454
+ #
455
+ # - An array containing the +:notify_flags+ that caused the callback
456
+ # execution.
457
+ # - The path of the current file.
458
+ # - A hash describing the baseline blob (or +nil+ if it does not
459
+ # exist).
460
+ # - A hash describing the target blob (or +nil+ if it does not exist).
461
+ # - A hash describing the workdir blob (or +nil+ if it does not
462
+ # exist).
463
+ #
464
+ # :strategy ::
465
+ # A single symbol or an array of symbols representing the strategies
466
+ # to use when performing the checkout. Possible values are:
467
+ #
468
+ # :none ::
469
+ # Perform a dry run (default).
470
+ #
471
+ # :safe ::
472
+ # Allow safe updates that cannot overwrite uncommitted data.
473
+ #
474
+ # :safe_create ::
475
+ # Allow safe updates plus creation of missing files.
476
+ #
477
+ # :force ::
478
+ # Allow all updates to force working directory to look like index.
479
+ #
480
+ # :allow_conflicts ::
481
+ # Allow checkout to make safe updates even if conflicts are found.
482
+ #
483
+ # :remove_untracked ::
484
+ # Remove untracked files not in index (that are not ignored).
485
+ #
486
+ # :remove_ignored ::
487
+ # Remove ignored files not in index.
488
+ #
489
+ # :update_only ::
490
+ # Only update existing files, don't create new ones.
491
+ #
492
+ # :dont_update_index ::
493
+ # Normally checkout updates index entries as it goes; this stops
494
+ # that.
495
+ #
496
+ # :no_refresh ::
497
+ # Don't refresh index/config/etc before doing checkout.
498
+ #
499
+ # :disable_pathspec_match ::
500
+ # Treat pathspec as simple list of exact match file paths.
501
+ #
502
+ # :skip_locked_directories ::
503
+ # Ignore directories in use, they will be left empty.
504
+ #
505
+ # :skip_unmerged ::
506
+ # Allow checkout to skip unmerged files (NOT IMPLEMENTED).
507
+ #
508
+ # :use_ours ::
509
+ # For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED).
510
+ #
511
+ # :use_theirs ::
512
+ # For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED).
513
+ #
514
+ # :update_submodules ::
515
+ # Recursively checkout submodules with same options (NOT
516
+ # IMPLEMENTED).
517
+ #
518
+ # :update_submodules_if_changed ::
519
+ # Recursively checkout submodules if HEAD moved in super repo (NOT
520
+ # IMPLEMENTED).
521
+ #
522
+ # :disable_filters ::
523
+ # If +true+, filters like CRLF line conversion will be disabled.
524
+ #
525
+ # :dir_mode ::
526
+ # Mode for newly created directories. Default: +0755+.
527
+ #
528
+ # :file_mode ::
529
+ # Mode for newly created files. Default: +0755+ or +0644+.
530
+ #
531
+ # :file_open_flags ::
532
+ # Mode for opening files. Default:
533
+ # <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>.
534
+ #
535
+ # :notify_flags ::
536
+ # A single symbol or an array of symbols representing the cases in
537
+ # which the +:notify+ callback should be invoked. Possible values are:
538
+ #
539
+ # :none ::
540
+ # Do not invoke the +:notify+ callback (default).
541
+ #
542
+ # :conflict ::
543
+ # Invoke the callback for conflicting paths.
544
+ #
545
+ # :dirty ::
546
+ # Invoke the callback for "dirty" files, i.e. those that do not need
547
+ # an update but no longer match the baseline.
548
+ #
549
+ # :updated ::
550
+ # Invoke the callback for any file that was changed.
551
+ #
552
+ # :untracked ::
553
+ # Invoke the callback for untracked files.
554
+ #
555
+ # :ignored ::
556
+ # Invoke the callback for ignored files.
557
+ #
558
+ # :all ::
559
+ # Invoke the callback for all these cases.
560
+ #
561
+ # :paths ::
562
+ # A glob string or an array of glob strings specifying which paths
563
+ # should be taken into account for the checkout operation. +nil+ will
564
+ # match all files. Default: +nil+.
565
+ #
566
+ # :baseline ::
567
+ # A Rugged::Tree that represents the current, expected contents of the
568
+ # workdir. Default: +HEAD+.
569
+ #
570
+ # :target_directory ::
571
+ # A path to an alternative workdir directory in which the checkout
572
+ # should be performed.
573
+ def checkout(ref, options = {}, start_point = "HEAD")
574
+ if options[:b]
575
+ rugged.branches.create(ref, start_point)
576
+ options.delete(:b)
577
+ end
578
+ default_options = { strategy: :safe_create }
579
+ rugged.checkout(ref, default_options.merge(options))
580
+ end
581
+
582
+ # Delete the specified branch from the repository
583
+ def delete_branch(branch_name)
584
+ rugged.branches.delete(branch_name)
585
+ end
586
+
587
+ # Return an array of this repository's remote names
588
+ def remote_names
589
+ rugged.remotes.each_name.to_a
590
+ end
591
+
592
+ # Delete the specified remote from this repository.
593
+ def remote_delete(remote_name)
594
+ rugged.remotes.delete(remote_name)
595
+ end
596
+
597
+ # Add a new remote to this repository. Returns a Rugged::Remote object
598
+ def remote_add(remote_name, url)
599
+ rugged.remotes.create(remote_name, url)
600
+ end
601
+
602
+ # Update the specified remote using the values in the +options+ hash
603
+ #
604
+ # Example
605
+ # repo.update_remote("origin", url: "path/to/repo")
606
+ def remote_update(remote_name, options = {})
607
+ # TODO: Implement other remote options
608
+ remote = rugged.remotes[remote_name]
609
+ remote.url = options[:url] if options[:url]
610
+ remote.save
611
+ end
612
+
613
+ # Fetch the specified remote
614
+ def fetch(remote_name)
615
+ rugged.remotes[remote_name].fetch
616
+ end
617
+
618
+ # Push +*refspecs+ to the remote identified by +remote_name+.
619
+ def push(remote_name, *refspecs)
620
+ rugged.remotes[remote_name].push(refspecs)
621
+ end
622
+
623
+ # Return a String containing the mbox-formatted diff between +from+ and
624
+ # +to+
625
+ def format_patch(from, to)
626
+ rugged.diff(from, to).patch
627
+ from_sha = rugged.rev_parse_oid(from)
628
+ to_sha = rugged.rev_parse_oid(to)
629
+ commits_between(from_sha, to_sha).map do |commit|
630
+ commit.to_mbox
631
+ end.join("\n")
632
+ end
633
+
634
+ # Merge the +source_name+ branch into the +target_name+ branch. This is
635
+ # equivalent to `git merge --no_ff +source_name+`, since a merge commit
636
+ # is always created.
637
+ def merge(source_name, target_name, options = {})
638
+ our_commit = rugged.branches[target_name].target
639
+ their_commit = rugged.branches[source_name].target
640
+
641
+ raise "Invalid merge target" if our_commit.nil?
642
+ raise "Invalid merge source" if their_commit.nil?
643
+
644
+ merge_index = rugged.merge_commits(our_commit, their_commit)
645
+ return false if merge_index.conflicts?
646
+
647
+ actual_options = options.merge(
648
+ parents: [our_commit, their_commit],
649
+ tree: merge_index.write_tree(rugged),
650
+ update_ref: "refs/heads/#{target_name}"
651
+ )
652
+ Rugged::Commit.create(rugged, actual_options)
653
+ end
654
+
655
+ def commits_since(from_date)
656
+ walker = Rugged::Walker.new(rugged)
657
+ walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
658
+
659
+ rugged.references.each("refs/heads/*") do |ref|
660
+ walker.push(ref.target_id)
661
+ end
662
+
663
+ commits = []
664
+ walker.each do |commit|
665
+ break if commit.author[:time].to_date < from_date
666
+ commits.push(commit)
667
+ end
668
+
669
+ commits
670
+ end
671
+
672
+ private
673
+
674
+ # Return the object that +revspec+ points to. If +revspec+ is an
675
+ # annotated tag, then return the tag's target instead.
676
+ def rev_parse_target(revspec)
677
+ obj = rugged.rev_parse(revspec)
678
+ if obj.is_a?(Rugged::Tag::Annotation)
679
+ obj.target
680
+ else
681
+ obj
682
+ end
683
+ end
684
+
685
+ # Get the content of a blob for a given tree. If the blob is a commit
686
+ # (for submodules) then return the blob's OID.
687
+ def blob_content(tree, blob_name)
688
+ blob_hash = tree.detect { |b| b[:name] == blob_name }
689
+
690
+ if blob_hash[:type] == :commit
691
+ blob_hash[:oid]
692
+ else
693
+ rugged.lookup(blob_hash[:oid]).content
694
+ end
695
+ end
696
+
697
+ # Parses the contents of a .gitmodules file and returns a hash of
698
+ # submodule information.
699
+ def parse_gitmodules(tree, content)
700
+ results = {}
701
+
702
+ current = ""
703
+ content.split("\n").each do |txt|
704
+ if txt.match(/^\[/)
705
+ current = txt.match(/(?<=").*(?=")/)[0]
706
+ results[current] = {}
707
+ else
708
+ match_data = txt.match(/(\w+) = (.*)/)
709
+ results[current][match_data[1]] = match_data[2]
710
+
711
+ if match_data[1] == "path"
712
+ results[current]["id"] = blob_content(tree, match_data[2])
713
+ end
714
+ end
715
+ end
716
+
717
+ results
718
+ end
719
+
720
+ # Return an array of log commits, given an SHA hash and a hash of
721
+ # options.
722
+ def build_log(sha, options)
723
+ # Instantiate a Walker and add the SHA hash
724
+ walker = Rugged::Walker.new(rugged)
725
+ walker.push(sha)
726
+
727
+ commits = []
728
+ skipped = 0
729
+ current_path = options[:path]
730
+
731
+ walker.each do |c|
732
+ break if options[:limit].to_i > 0 &&
733
+ commits.length >= options[:limit].to_i
734
+
735
+ if !current_path ||
736
+ commit_touches_path?(c, current_path, options[:follow])
737
+
738
+ # This is a commit we care about, unless we haven't skipped enough
739
+ # yet
740
+ skipped += 1
741
+ commits.push(c) if skipped > options[:offset]
742
+ end
743
+ end
744
+
745
+ walker.reset
746
+
747
+ commits
748
+ end
749
+
750
+ # Returns true if the given commit affects the given path. If the
751
+ # +follow+ option is true and the file specified by +path+ was renamed,
752
+ # then the path value is set to the old path.
753
+ def commit_touches_path?(commit, path, follow)
754
+ if commit.parents.empty?
755
+ diff = commit.diff
756
+ else
757
+ diff = commit.parents[0].diff(commit)
758
+ diff.find_similar! if follow
759
+ end
760
+
761
+ # Check the commit's deltas to see if it touches the :path
762
+ # argument
763
+ diff.each_delta do |d|
764
+ if path_matches?(path, d.old_file[:path], d.new_file[:path])
765
+ if should_follow?(follow, d, path)
766
+ # Look for the old path in ancestors
767
+ path.replace(d.old_file[:path])
768
+ end
769
+
770
+ return true
771
+ end
772
+ end
773
+
774
+ false
775
+ end
776
+
777
+ # Used by the #commit_touches_path method to determine whether the
778
+ # specified file has been renamed and should be followed in ancestor
779
+ # commits. Returns true if +follow_option+ is true, the file is renamed
780
+ # in this commit, and the new file's path matches the path option.
781
+ def should_follow?(follow_option, delta, path)
782
+ follow_option && delta.renamed? && path == delta.new_file[:path]
783
+ end
784
+
785
+ # Returns true if any of the strings in +*paths+ begins with the
786
+ # +path_to_match+ argument
787
+ def path_matches?(path_to_match, *paths)
788
+ paths.any? do |p|
789
+ p.match(/^#{Regexp.escape(path_to_match)}/)
790
+ end
791
+ end
792
+
793
+ # Create an archive with the repository's files
794
+ def create_archive(ref_name, pipe_cmd, file_path)
795
+ # Put files into a prefix directory in the archive
796
+ prefix = File.basename(name)
797
+ extension = Pathname.new(file_path).extname
798
+
799
+ if extension == ".zip"
800
+ create_zip_archive(ref_name, file_path, prefix)
801
+ else
802
+ # Create a tarfile in memory
803
+ tarfile = tar_string_io(ref_name, prefix)
804
+
805
+ if extension == ".tar"
806
+ File.new(file_path, "wb").write(tarfile.read)
807
+ else
808
+ compress_tar(tarfile, file_path, pipe_cmd)
809
+ end
810
+ end
811
+ end
812
+
813
+ # Return a StringIO with the contents of the repo's tar file
814
+ def tar_string_io(ref_name, prefix)
815
+ tarfile = StringIO.new
816
+ Gem::Package::TarWriter.new(tarfile) do |tar|
817
+ tar.mkdir(prefix, 33261)
818
+
819
+ populated_index(ref_name).each do |entry|
820
+ add_archive_entry(tar, prefix, entry)
821
+ end
822
+ end
823
+
824
+ tarfile.rewind
825
+ tarfile
826
+ end
827
+
828
+ # Create a zip file with the contents of the repo
829
+ def create_zip_archive(ref_name, archive_path, prefix)
830
+ Zip::File.open(archive_path, Zip::File::CREATE) do |zipfile|
831
+ populated_index(ref_name).each do |entry|
832
+ add_archive_entry(zipfile, prefix, entry)
833
+ end
834
+ end
835
+ end
836
+
837
+ # Add a file or directory from the index to the given tar or zip file
838
+ def add_archive_entry(archive, prefix, entry)
839
+ prefixed_path = File.join(prefix, entry[:path])
840
+ content = rugged.lookup(entry[:oid]).content unless submodule?(entry)
841
+
842
+ # Create a file in the archive for each index entry
843
+ if archive.is_a?(Zip::File)
844
+ unless submodule?(entry)
845
+ archive.get_output_stream(prefixed_path) do |os|
846
+ os.write(content)
847
+ end
848
+ end
849
+ else
850
+ if submodule?(entry)
851
+ # Create directories for submodules
852
+ archive.mkdir(prefixed_path, 33261)
853
+ else
854
+ # Write the blob contents to the file
855
+ archive.add_file(prefixed_path, entry[:mode]) do |tf|
856
+ tf.write(content)
857
+ end
858
+ end
859
+ end
860
+ end
861
+
862
+ # Returns true if the index entry has the special file mode that denotes
863
+ # a submodule.
864
+ def submodule?(index_entry)
865
+ index_entry[:mode] == 57344
866
+ end
867
+
868
+ # Send the +tar_string+ StringIO to +pipe_cmd+ for bzip2 or gzip
869
+ # compression.
870
+ def compress_tar(tar_string, file_path, pipe_cmd)
871
+ # Write the in-memory tarfile to a pipe
872
+ rd_pipe, rw_pipe = IO.pipe
873
+ tar_pid = fork do
874
+ rd_pipe.close
875
+ rw_pipe.write(tar_string.read)
876
+ rw_pipe.close
877
+ end
878
+
879
+ # Use the other end of the pipe to compress with bzip2 or gzip
880
+ FileUtils.mkdir_p(Pathname.new(file_path).dirname)
881
+ archive_file = File.new(file_path, "wb")
882
+ rw_pipe.close
883
+ compress_pid = spawn(*pipe_cmd, in: rd_pipe, out: archive_file)
884
+ rd_pipe.close
885
+
886
+ Process.waitpid(tar_pid)
887
+ Process.waitpid(compress_pid)
888
+
889
+ archive_file.close
890
+ tar_string.close
891
+ end
892
+
893
+ # Return a Rugged::Index that has read from the tree at +ref_name+
894
+ def populated_index(ref_name)
895
+ tree = rugged.lookup(rugged.rev_parse_oid(ref_name)).tree
896
+ index = rugged.index
897
+ index.read_tree(tree)
898
+ index
899
+ end
900
+
901
+ # Return an array of BlobSnippets for lines in +file_contents+ that match
902
+ # +query+
903
+ def build_greps(file_contents, query, ref, filename)
904
+ greps = []
905
+
906
+ file_contents.split("\n").each_with_index do |line, i|
907
+ next unless line.match(/#{Regexp.escape(query)}/i)
908
+
909
+ greps << Gitlab::Git::BlobSnippet.new(
910
+ ref,
911
+ file_contents.split("\n")[i - 3..i + 3],
912
+ i - 2,
913
+ filename
914
+ )
915
+ end
916
+
917
+ greps
918
+ end
377
919
  end
378
920
  end
379
921
  end