gitlab_git 6.3.0 → 7.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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