Class: Braid::Mirror

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

Defined Under Namespace

Classes: NoTagAndBranch, Options, 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.

T.let(%w(url branch path tag revision), T::Array[String])
MirrorAttributes =

It’s going to take significant refactoring to be able to give ‘MirrorAttributes` a type. Encapsulating the `T.untyped` in a type alias makes it easier to search for all the distinct root causes of untypedness in the code.

T.type_alias { T::Hash[String, T.untyped] }
BreakingChangeCallback =
T.type_alias { T.proc.params(arg0: String).void }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from T::Sig

sig

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.



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
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/braid/mirror.rb', line 41

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

  # 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

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



36
37
38
# File 'lib/braid/mirror.rb', line 36

def attributes
  @attributes
end

#pathObject (readonly)

Returns the value of attribute path.



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

def path
  @path
end

Class Method Details

.new_from_options(url, options = Options.new) ⇒ Object

Raises:



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/braid/mirror.rb', line 95

def self.new_from_options(url, options = Options.new)
  url    = url.sub(/\/$/, '')
  # TODO: Ensure `url` is absolute?  The user is probably more likely to
  # move the downstream repository by itself than to move it along with the
  # vendor repository.  And we definitely don't want to use relative URLs in
  # the cache.

  raise NoTagAndBranch if options.tag && options.branch

  tag = options.tag
  branch = options.branch

  path = (options.path || extract_path_from_url(url, options.remote_path)).sub(/\/$/, '')
  # TODO: Check that `path` is a valid relative path and not something like
  # '.' or ''.  Some of these pathological cases will cause Braid to bail
  # out later when something else fails, but it would be better to check up
  # front.

  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



120
121
122
# File 'lib/braid/mirror.rb', line 120

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

#base_revisionObject



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/braid/mirror.rb', line 232

def base_revision
  # TODO (typing): We think `revision` should always be non-nil here these
  # days and we can completely drop the `inferred_revision` code, but we're
  # waiting for a better time to actually make this runtime behavior change
  # and accept any risk of breakage
  # (https://github.com/cristibalan/braid/pull/105/files#r857150464).
  #
  # Temporary variable
  # (https://sorbet.org/docs/flow-sensitive#limitations-of-flow-sensitivity)
  revision1 = revision
  if revision1
    git.rev_parse(revision1)
  else
    # NOTE: Given that `inferred_revision` does appear to return nil on one
    # code path, using this `T.must` and giving `base_revision` a
    # non-nilable return type presents a theoretical risk of leading us to
    # make changes to callers that break things at runtime.  But we judge
    # this a lesser evil than making the return type nilable and changing
    # all callers to type-check successfully with that when we hope to
    # revert the change soon anyway.
    T.must(inferred_revision)
  end
end

#branchObject



294
295
296
# File 'lib/braid/mirror.rb', line 294

def branch
  self.attributes['branch']
end

#branch=(new_value) ⇒ Object



299
300
301
# File 'lib/braid/mirror.rb', line 299

def branch=(new_value)
  self.attributes['branch'] = new_value
end

#cached?Boolean

Returns:

  • (Boolean)


227
228
229
# File 'lib/braid/mirror.rb', line 227

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

#cached_urlObject



336
337
338
# File 'lib/braid/mirror.rb', line 336

def cached_url
  git_cache.path(url)
end

#diffObject



199
200
201
202
# File 'lib/braid/mirror.rb', line 199

def diff
  fetch_base_revision_if_missing
  git.diff(diff_args)
end

#diff_args(user_args = []) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/braid/mirror.rb', line 148

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(T.must(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



221
222
223
224
# File 'lib/braid/mirror.rb', line 221

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

#fetch_base_revision_if_missingObject



210
211
212
213
214
215
216
217
218
# File 'lib/braid/mirror.rb', line 210

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



257
258
259
260
261
262
# File 'lib/braid/mirror.rb', line 257

def local_ref
  return "#{self.remote}/#{self.branch}" unless self.branch.nil?
  return "tags/#{self.tag}" unless self.tag.nil?
  # TODO (typing): Remove this `T.must` if we make `revision` non-nilable.
  T.must(self.revision)
end

#locked?Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/braid/mirror.rb', line 125

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

#merged?(commit) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
133
134
135
# File 'lib/braid/mirror.rb', line 130

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



341
342
343
344
345
346
347
348
# File 'lib/braid/mirror.rb', line 341

def remote
  # Ensure that we replace any characters in the mirror path that might be
  # problematic in a Git ref name.  Theoretically, this may introduce
  # collisions between mirrors, but we don't expect that to be much of a
  # problem because Braid doesn't keep remotes by default after a command
  # exits.
  "#{branch || tag || 'revision'}_braid_#{path}".gsub(/[^-A-Za-z0-9]/, '_')
end

#remote_pathObject



304
305
306
# File 'lib/braid/mirror.rb', line 304

def remote_path
  self.attributes['path']
end

#remote_path=(remote_path) ⇒ Object



309
310
311
# File 'lib/braid/mirror.rb', line 309

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

#remote_refObject



268
269
270
# File 'lib/braid/mirror.rb', line 268

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

#revisionObject



326
327
328
# File 'lib/braid/mirror.rb', line 326

def revision
  self.attributes['revision']
end

#revision=(new_value) ⇒ Object



331
332
333
# File 'lib/braid/mirror.rb', line 331

def revision=(new_value)
  self.attributes['revision'] = new_value
end

#tagObject



314
315
316
# File 'lib/braid/mirror.rb', line 314

def tag
  self.attributes['tag']
end

#tag=(new_value) ⇒ Object



319
320
321
# File 'lib/braid/mirror.rb', line 319

def tag=(new_value)
  self.attributes['tag'] = new_value
end

#upstream_item_for_revision(revision) ⇒ Object



138
139
140
# File 'lib/braid/mirror.rb', line 138

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

#urlObject



284
285
286
# File 'lib/braid/mirror.rb', line 284

def url
  self.attributes['url']
end

#url=(new_value) ⇒ Object



289
290
291
# File 'lib/braid/mirror.rb', line 289

def url=(new_value)
  self.attributes['url'] = new_value
end