Class: Braid::Mirror

Inherits:
Object
  • Object
show all
Includes:
Operations::VersionControl
Defined in:
lib/braid/mirror.rb

Defined Under Namespace

Classes: NoTagAndBranch, PathRequired, UnknownType

Constant Summary collapse

ATTRIBUTES =

Since Braid 1.1.0, the attributes are written to .braids.json in this canonical order. For now, the order is chosen to match what Braid 1.0.22 produced for newly added mirrors.

%w(url branch path tag revision)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Operations::VersionControl

#git, #git_cache

Constructor Details

#initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB) ⇒ Mirror

Returns a new instance of Mirror.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/braid/mirror.rb', line 28

def initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB)
  @path       = path.sub(/\/$/, '')
  @attributes = attributes.dup

  # Not that it's terribly important to check for such an old feature.  This
  # is mainly to demonstrate the RemoveMirrorDueToBreakingChange mechanism
  # in case we want to use it for something else in the future.
  if !@attributes['type'].nil? && @attributes['type'] != 'git'
    breaking_change_cb.call <<-DESC
- Mirror '#{path}' is of a Subversion repository, which is no
  longer supported.  The mirror will be removed from your configuration, leaving
  the data in the tree.
DESC
    raise Config::RemoveMirrorDueToBreakingChange
  end
  @attributes.delete('type')

  # Migrate revision locks from Braid < 1.0.18.  We no longer store the
  # original branch or tag (the user has to specify it again when
  # unlocking); we simply represent a locked revision by the absence of a
  # branch or tag.
  if @attributes['lock']
    @attributes.delete('lock')
    @attributes['branch'] = nil
    @attributes['tag'] = nil
  end

  # Removal of support for full-history mirrors from Braid < 1.0.17 is a
  # breaking change for users who wanted to use the imported history in some
  # way.
  if !@attributes['squashed'].nil? && @attributes['squashed'] != true
    breaking_change_cb.call <<-DESC
- Mirror '#{path}' is full-history, which is no longer supported.
  It will be changed to squashed.  Upstream history already imported will remain
  in your project's history and will have no effect on Braid.
DESC
  end
  @attributes.delete('squashed')
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object (private)



229
230
231
232
233
234
235
236
237
238
239
# File 'lib/braid/mirror.rb', line 229

def method_missing(name, *args)
  if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
    if $2
      attributes[$1] = args[0]
    else
      attributes[$1]
    end
  else
    raise NameError, "unknown attribute `#{name}'"
  end
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



26
27
28
# File 'lib/braid/mirror.rb', line 26

def attributes
  @attributes
end

#pathObject (readonly)

Returns the value of attribute path.



26
27
28
# File 'lib/braid/mirror.rb', line 26

def path
  @path
end

Class Method Details

.new_from_options(url, options = {}) ⇒ Object

Raises:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/braid/mirror.rb', line 68

def self.new_from_options(url, options = {})
  url    = url.sub(/\/$/, '')

  raise NoTagAndBranch if options['tag'] && options['branch']

  tag = options['tag']
  branch = options['branch'] || (tag.nil? ? 'master' : nil)

  path = (options['path'] || extract_path_from_url(url, options['remote_path'])).sub(/\/$/, '')
  raise PathRequired unless path

  remote_path = options['remote_path']

  attributes = {'url' => url, 'branch' => branch, 'path' => remote_path, 'tag' => tag}
  self.new(path, attributes)
end

Instance Method Details

#==(comparison) ⇒ Object



85
86
87
# File 'lib/braid/mirror.rb', line 85

def ==(comparison)
  path == comparison.path && attributes == comparison.attributes
end

#base_revisionObject



188
189
190
191
192
193
194
# File 'lib/braid/mirror.rb', line 188

def base_revision
  if revision
    git.rev_parse(revision)
  else
    inferred_revision
  end
end

#cached?Boolean

Returns:

  • (Boolean)


184
185
186
# File 'lib/braid/mirror.rb', line 184

def cached?
  git.remote_url(remote) == cached_url
end

#cached_urlObject



214
215
216
# File 'lib/braid/mirror.rb', line 214

def cached_url
  git_cache.path(url)
end

#diffObject

Precondition: the remote for this mirror is set up.



159
160
161
162
# File 'lib/braid/mirror.rb', line 159

def diff
  fetch_base_revision_if_missing
  git.diff(diff_args)
end

#diff_args(user_args = []) ⇒ Object

Return the arguments that should be passed to “git diff” to diff this mirror (including uncommitted changes by default), incorporating the given user-specified arguments. Having the caller run “git diff” is convenient for now but violates encapsulation a little; we may have to reorganize the code in order to add features.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/braid/mirror.rb', line 109

def diff_args(user_args = [])
  upstream_item = upstream_item_for_revision(base_revision)

  # We do not need to spend the time to copy the content outside the
  # mirror from HEAD because --relative will exclude it anyway.  Rename
  # detection seems to apply only to the files included in the diff, so we
  # shouldn't have another bug like
  # https://github.com/cristibalan/braid/issues/41.
  base_tree = git.make_tree_with_item(nil, path, upstream_item)

  # Note: --relative does a naive prefix comparison.  If we set (for
  # example) `--relative=a/b`, that will match an unrelated file or
  # directory name `a/bb`.  If the mirror is a directory, we can avoid this
  # by adding a trailing slash to the prefix.
  #
  # If the mirror is a file, the only way we can avoid matching a path like
  # `a/bb` is to pass a path argument to limit the diff.  This means if the
  # user passes additional path arguments, we won't get the behavior we
  # expect, which is the intersection of the user-specified paths with the
  # mirror.  However, it's probably unreasonable for a user to pass path
  # arguments when diffing a single-file mirror, so we ignore the issue.
  #
  # Note: This code doesn't handle various cases in which a directory at the
  # root of a mirror turns into a file or vice versa.  If that happens,
  # hopefully the user takes corrective action manually.
  if upstream_item.is_a?(git.BlobWithMode)
    # For a single-file mirror, we use the upstream basename for the
    # upstream side of the diff and the downstream basename for the
    # downstream side, like what `git diff` does when given two blobs as
    # arguments.  Use --relative to strip away the entire downstream path
    # before we add the basenames.
    return [
      '--relative=' + path,
      '--src-prefix=a/' + File.basename(remote_path),
      '--dst-prefix=b/' + File.basename(path),
      base_tree,
      # user_args may contain options, which must come before paths.
      *user_args,
      path
    ]
  else
    return [
      '--relative=' + path + '/',
      base_tree,
      *user_args
    ]
  end
end

#fetchObject



179
180
181
182
# File 'lib/braid/mirror.rb', line 179

def fetch
  git_cache.fetch(url) if cached?
  git.fetch(remote)
end

#fetch_base_revision_if_missingObject

Re-fetching the remote after deleting and re-adding it may be slow even if all objects are still present in the repository (github.com/cristibalan/braid/issues/71). Mitigate this for ‘braid diff` and other commands that need the diff by skipping the fetch if the base revision is already present in the repository.



169
170
171
172
173
174
175
176
177
# File 'lib/braid/mirror.rb', line 169

def fetch_base_revision_if_missing
  begin
    # Without ^{commit}, this will happily pass back an object hash even if
    # the object isn't present.  See the git-rev-parse(1) man page.
    git.rev_parse(base_revision + '^{commit}')
  rescue Operations::UnknownRevision
    fetch
  end
end

#local_refObject



196
197
198
199
200
# File 'lib/braid/mirror.rb', line 196

def local_ref
  return "#{self.remote}/#{self.branch}" unless self.branch.nil?
  return "tags/#{self.tag}" unless self.tag.nil?
  self.revision
end

#locked?Boolean

Returns:

  • (Boolean)


89
90
91
# File 'lib/braid/mirror.rb', line 89

def locked?
  branch.nil? && tag.nil?
end

#merged?(commit) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
96
97
98
# File 'lib/braid/mirror.rb', line 93

def merged?(commit)
  # tip from spearce in #git:
  # `test z$(git merge-base A B) = z$(git rev-parse --verify A)`
  commit = git.rev_parse(commit)
  !!base_revision && git.merge_base(commit, base_revision) == commit
end

#remoteObject



218
219
220
# File 'lib/braid/mirror.rb', line 218

def remote
  "#{branch || tag || 'revision'}/braid/#{path}".gsub(/\/\./, '/_')
end

#remote_pathObject



206
207
208
# File 'lib/braid/mirror.rb', line 206

def remote_path
  self.attributes['path']
end

#remote_path=(remote_path) ⇒ Object



210
211
212
# File 'lib/braid/mirror.rb', line 210

def remote_path=(remote_path)
  self.attributes['path'] = remote_path
end

#remote_refObject



202
203
204
# File 'lib/braid/mirror.rb', line 202

def remote_ref
  self.branch.nil? ? "+refs/tags/#{self.tag}" : "+refs/heads/#{self.branch}"
end

#upstream_item_for_revision(revision) ⇒ Object



100
101
102
# File 'lib/braid/mirror.rb', line 100

def upstream_item_for_revision(revision)
  git.get_tree_item(revision, self.remote_path)
end