Class: GitReview::Local

Inherits:
Object
  • Object
show all
Includes:
Helpers
Defined in:
lib/git-review/local.rb

Overview

The local repository is where the git-review command is being called by default. It is (supposedly) able to handle systems other than Github. TODO: remove Github-dependency

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeLocal

Returns a new instance of Local.



18
19
20
21
22
23
24
25
# File 'lib/git-review/local.rb', line 18

def initialize
  # find root git directory if currently in subdirectory
  if git_call('rev-parse --show-toplevel').strip.empty?
    raise ::GitReview::InvalidGitRepositoryError
  else
    load_config
  end
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



10
11
12
# File 'lib/git-review/local.rb', line 10

def config
  @config
end

Class Method Details

.instanceObject

acts like a singleton class but it’s actually not use ::GitReview::Local.instance everywhere except in tests



14
15
16
# File 'lib/git-review/local.rb', line 14

def self.instance
  @instance ||= new
end

Instance Method Details

#all_branchesArray<String>

Returns all existing branches.

Returns:

  • (Array<String>)

    all existing branches



111
112
113
# File 'lib/git-review/local.rb', line 111

def all_branches
  git_call('branch -a').gsub('* ', '').split("\n").collect { |s| s.strip }
end

#branch_exists?(location, branch_name) ⇒ Boolean

Returns whether a branch exists in a specified location.

Parameters:

  • location (Symbol)

    location of the branch, ‘:remote` or `:local`

  • branch_name (String)

    name of the branch

Returns:

  • (Boolean)

    whether a branch exists in a specified location



180
181
182
183
184
# File 'lib/git-review/local.rb', line 180

def branch_exists?(location, branch_name)
  return false unless [:remote, :local].include?(location)
  prefix = location == :remote ? 'remotes/origin/' : ''
  all_branches.include?(prefix + branch_name)
end

#clean_allObject

clean all obsolete branches



147
148
149
150
151
152
# File 'lib/git-review/local.rb', line 147

def clean_all
  (review_branches - protected_branches).each do |branch_name|
    # only clean up obsolete branches.
    delete_branch(branch_name) unless unmerged_commits?(branch_name, false)
  end
end

#clean_remotesObject

Remove obsolete remotes with review prefix.



75
76
77
78
79
80
81
82
83
# File 'lib/git-review/local.rb', line 75

def clean_remotes
  protected_remotes = remotes_for_branches
  remotes.each do |remote|
    # Only remove review remotes that aren't referenced by current branches.
    if remote.index('review_') == 0 && !protected_remotes.include?(remote)
      git_call "remote remove #{remote}"
    end
  end
end

#clean_single(number, force = false) ⇒ Object

clean a single request’s obsolete branch



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/git-review/local.rb', line 130

def clean_single(number, force = false)
  request = server.pull_request(source_repo, number)
  if request && request.state == 'closed'
    # ensure there are no unmerged commits or '--force' flag has been set
    branch_name = request.head.ref
    if unmerged_commits?(branch_name) && !force
      puts "Won't delete branches that contain unmerged commits."
      puts "Use '--force' to override."
    else
      delete_branch(branch_name)
    end
  end
rescue Octokit::NotFound
  false
end

#config_listObject



313
314
315
# File 'lib/git-review/local.rb', line 313

def config_list
  git_call('config --list', false)
end

#create_title_and_body(target_branch) ⇒ Array(String, String)

Returns the title and the body of pull request.

Returns:

  • (Array(String, String))

    the title and the body of pull request



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/git-review/local.rb', line 322

def create_title_and_body(target_branch)
   = server.
  commits = git_call("log --format='%H' HEAD...#{target_branch}").
    lines.count
  puts "Commits: #{commits}"
  if commits == 1
    # we can create a really specific title and body
    title = git_call("log --format='%s' HEAD...#{target_branch}").chomp
    body  = git_call("log --format='%b' HEAD...#{target_branch}").chomp
  else
    title = "[Review] Request from '#{}' @ '#{source}'"
    body  = "Please review the following changes:\n"
    body += git_call("log --oneline HEAD...#{target_branch}").
      lines.map{|l| "  * #{l.chomp}"}.join("\n")
  end
  edit_title_and_body(title, body)
end

#delete_branch(branch_name) ⇒ Object

delete local and remote branches that match a given name

Parameters:

  • branch_name (String)

    name of the branch to delete



156
157
158
159
# File 'lib/git-review/local.rb', line 156

def delete_branch(branch_name)
  delete_local_branch(branch_name)
  delete_remote_branch(branch_name)
end

#delete_local_branch(branch_name) ⇒ Object

delete local branch if it exists.

Parameters:

  • branch_name (String)

    name of the branch to delete



163
164
165
166
167
# File 'lib/git-review/local.rb', line 163

def delete_local_branch(branch_name)
  if branch_exists?(:local, branch_name)
    git_call("branch -D #{branch_name}", true)
  end
end

#delete_remote_branch(branch_name) ⇒ Object

delete remote branch if it exists.

Parameters:

  • branch_name (String)

    name of the branch to delete



171
172
173
174
175
# File 'lib/git-review/local.rb', line 171

def delete_remote_branch(branch_name)
  if branch_exists?(:remote, branch_name)
    git_call("push origin :#{branch_name}", true)
  end
end

#edit_title_and_body(title, body) ⇒ Object

TODO: refactor



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/git-review/local.rb', line 341

def edit_title_and_body(title, body)
  tmpfile = Tempfile.new('git-review')
  tmpfile.write(title + "\n\n" + body)
  tmpfile.flush
  editor = ENV['TERM_EDITOR'] || ENV['EDITOR']
  unless editor
    warn 'Please set $EDITOR or $TERM_EDITOR in your .bash_profile.'
  end

  system("#{editor || 'open'} #{tmpfile.path}")

  tmpfile.rewind
  lines = tmpfile.read.lines.to_a
  #puts lines.inspect
  title = lines.shift.chomp
  lines.shift if lines[0].chomp.empty?
  body = lines.join
  tmpfile.unlink
  [title, body]
end

#headString

Returns the head string used for pull requests.

Returns:

  • (String)

    the head string used for pull requests



282
283
284
285
# File 'lib/git-review/local.rb', line 282

def head
  # in the form of 'user:branch'
  "#{source_repo.split('/').first}:#{source_branch}"
end

#load_configObject



300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/git-review/local.rb', line 300

def load_config
  @config = {}
  config_list.split("\n").each do |line|
    key, value = line.split(/=/, 2)
    if @config[key] && @config[key] != value
      @config[key] = [@config[key]].flatten << value
    else
      @config[key] = value
    end
  end
  @config
end

#merged?(sha) ⇒ Boolean

Returns whether a specified commit has already been merged.

Returns:

  • (Boolean)

    whether a specified commit has already been merged.



238
239
240
241
242
# File 'lib/git-review/local.rb', line 238

def merged?(sha)
  branches = git_call("branch --contains #{sha} 2>&1").split("\n").
      collect { |b| b.delete('*').strip }
  branches.include?(target_branch)
end

#new_commits?(upstream = false) ⇒ Boolean

Returns whether there are commits not in target branch yet.

Returns:

  • (Boolean)

    whether there are commits not in target branch yet



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/git-review/local.rb', line 224

def new_commits?(upstream = false)
  # Check if an upstream remote exists and create it if necessary.
  remote_url = server.remote_url_for(*target_repo(upstream).split('/'))
  remote = remotes_for_url(remote_url).first
  unless remote
    remote = 'upstream'
    git_call "remote add #{remote} #{remote_url}"
  end
  git_call "fetch #{remote}"
  target = upstream ? "#{remote}/#{target_branch}" : target_branch
  not git_call("cherry #{target}").empty?
end

#on_feature_branch?Boolean

Returns whether already on a feature branch.

Returns:

  • (Boolean)

    whether already on a feature branch



288
289
290
291
292
293
# File 'lib/git-review/local.rb', line 288

def on_feature_branch?
  # If current and target are the same, we are not on a feature branch.
  # If they are different, but we are on master, we should still to switch
  # to a separate branch (since master makes for a poor feature branch).
  source_branch != target_branch && source_branch != 'master'
end

#protected_branchesArray<String>

Returns all open requests’ branches shouldn’t be deleted.

Returns:

  • (Array<String>)

    all open requests’ branches shouldn’t be deleted



116
117
118
# File 'lib/git-review/local.rb', line 116

def protected_branches
  server.current_requests.collect { |r| r.head.ref }
end

#prune_remotesObject

Prune all configured remotes.



86
87
88
# File 'lib/git-review/local.rb', line 86

def prune_remotes
  remotes.each { |remote| git_call "remote prune #{remote}" }
end

#remote_exists?(name) ⇒ Boolean

Determine whether a remote with a given name exists?

Returns:

  • (Boolean)


33
34
35
# File 'lib/git-review/local.rb', line 33

def remote_exists?(name)
  remotes.include? name
end

#remote_for_branch(branch_name) ⇒ Object

Finds the correct remote for a given branch name.



99
100
101
102
103
104
105
106
107
108
# File 'lib/git-review/local.rb', line 99

def remote_for_branch(branch_name)
  git_call('branch -lvv').gsub('* ', '').split("\n").each do |line|
    entries = line.split(' ')
    next unless entries.first == branch_name
    # Return the remote name or nil for local branches.
    match = entries[2].match(%r(\[(.*)(\]|:)))
    return match[1].split('/').first if match
  end
  nil
end

#remote_for_request(request) ⇒ Object

Find or create the correct remote for a fork with a given owner name.



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/git-review/local.rb', line 61

def remote_for_request(request)
  repo_owner = request.head.repo.owner.
  remote_url = server.remote_url_for(repo_owner)
  remotes = remotes_for_url(remote_url)
  if remotes.empty?
    remote = "review_#{repo_owner}"
    git_call("remote add #{remote} #{remote_url}", debug_mode, true)
  else
    remote = remotes.first
  end
  remote
end

#remotesObject

List all available remotes.



28
29
30
# File 'lib/git-review/local.rb', line 28

def remotes
  git_call('remote').split("\n")
end

#remotes_for_branchesObject

Find all remotes which are currently referenced by local branches.



91
92
93
94
95
96
# File 'lib/git-review/local.rb', line 91

def remotes_for_branches
  remotes = git_call('branch -lvv').gsub('* ', '').split("\n").map do |line|
    line.split(' ')[2][1..-2].split('/').first
  end
  remotes.uniq
end

#remotes_for_url(remote_url) ⇒ Object

Collect all remotes for a given url.



53
54
55
56
57
58
# File 'lib/git-review/local.rb', line 53

def remotes_for_url(remote_url)
  result = remotes_with_urls.collect do |remote, urls|
    remote if urls.values.all? { |url| url == remote_url }
  end
  result.compact
end

#remotes_with_urlsObject

Create a Hash with all remotes as keys and their urls as values.



38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/git-review/local.rb', line 38

def remotes_with_urls
  result = {}
  git_call('remote -vv').split("\n").each do |line|
    entries = line.split("\t")
    remote = entries.first
    target_entry = entries.last.split(' ')
    direction = target_entry.last[1..-2].to_sym
    target_url = target_entry.first
    result[remote] ||= {}
    result[remote][direction] = target_url
  end
  result
end

#review_branchesArray<String>

Returns all review branches with ‘review_’ prefix.

Returns:

  • (Array<String>)

    all review branches with ‘review_’ prefix



121
122
123
124
125
126
127
# File 'lib/git-review/local.rb', line 121

def review_branches
  all_branches.collect { |entry|
    # only use uniq branch names (no matter if local or remote)
    branch_name = entry.split('/').last
    branch_name if branch_name.index('review_') == 0
  }.compact.uniq
end

#sanitize_branch_name(name) ⇒ Object

Remove all non word characters and turn them into underscores.



296
297
298
# File 'lib/git-review/local.rb', line 296

def sanitize_branch_name(name)
  name.gsub(/\W+/, '_').downcase
end

#serverObject



317
318
319
# File 'lib/git-review/local.rb', line 317

def server
  @server ||= ::GitReview::Server.instance
end

#sourceString

Returns combine source repo and branch.

Returns:

  • (String)

    combine source repo and branch



255
256
257
# File 'lib/git-review/local.rb', line 255

def source
  "#{source_repo}/#{source_branch}"
end

#source_branchString

Returns the current source branch.

Returns:

  • (String)

    the current source branch



250
251
252
# File 'lib/git-review/local.rb', line 250

def source_branch
  git_call('branch').chomp.match(/\*(.*)/)[0][2..-1]
end

#source_repoString

Returns the source repo.

Returns:

  • (String)

    the source repo



245
246
247
# File 'lib/git-review/local.rb', line 245

def source_repo
  server.source_repo
end

#targetString

Returns combine target repo and branch.

Returns:

  • (String)

    combine target repo and branch



277
278
279
# File 'lib/git-review/local.rb', line 277

def target
  "#{target_repo}/#{target_branch}"
end

#target_branchString

Returns the name of the target branch.

Returns:

  • (String)

    the name of the target branch



260
261
262
263
# File 'lib/git-review/local.rb', line 260

def target_branch
  # TODO: Manually override this and set arbitrary branches
  ENV['TARGET_BRANCH'] || 'master'
end

#target_repo(upstream = false) ⇒ String

if to send a pull request to upstream repo, get the parent as target

Returns:

  • (String)

    the name of the target repo



267
268
269
270
271
272
273
274
# File 'lib/git-review/local.rb', line 267

def target_repo(upstream=false)
  # TODO: Manually override this and set arbitrary repositories
  if upstream
    server.repository(source_repo).parent.full_name
  else
    source_repo
  end
end

#uncommitted_changes?Boolean

Returns whether there are local changes not committed.

Returns:

  • (Boolean)

    whether there are local changes not committed



187
188
189
# File 'lib/git-review/local.rb', line 187

def uncommitted_changes?
  !git_call('diff HEAD').empty?
end

#unmerged_commits?(branch_name, verbose = true) ⇒ Boolean

Returns whether there are unmerged commits on the local or remote branch.

Parameters:

  • branch_name (String)

    name of the branch

  • verbose (Boolean) (defaults to: true)

    if verbose output

Returns:

  • (Boolean)

    whether there are unmerged commits on the local or remote branch.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/git-review/local.rb', line 195

def unmerged_commits?(branch_name, verbose=true)
  locations = []
  locations << '' if branch_exists?(:local, branch_name)
  locations << 'origin/' if branch_exists?(:remote, branch_name)
  locations = locations.repeated_permutation(2).to_a
  if locations.empty?
    puts 'Nothing to do. All cleaned up already.' if verbose
    return false
  end
  # compare remote and local branch with remote and local master
  responses = locations.collect { |loc|
    git_call "cherry #{loc.first}#{target_branch} #{loc.last}#{branch_name}"
  }
  # select commits (= non empty, not just an error message and not only
  #   duplicate commits staring with '-').
  unmerged_commits = responses.reject { |response|
    response.empty? or response.include?('fatal: Unknown commit') or
        response.split("\n").reject { |x| x.index('-') == 0 }.empty?
  }
  # if the array ain't empty, we got unmerged commits
  if unmerged_commits.empty?
    false
  else
    puts "Unmerged commits on branch '#{branch_name}'."
    true
  end
end