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, 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.

T.let(%w(url branch path tag revision), T::Array[String])
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.



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
80
# File 'lib/braid/mirror.rb', line 42

def initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB)
  @path       = T.let(path.sub(/\/$/, ''), String)
  @attributes = T.let(attributes.dup, T::Hash[String, T.untyped])

  # 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.



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

def attributes
  @attributes
end

#pathObject (readonly)

Returns the value of attribute path.



33
34
35
# File 'lib/braid/mirror.rb', line 33

def path
  @path
end

Class Method Details

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

Raises:



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/braid/mirror.rb', line 83

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

  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(/\/$/, '')
  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



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

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

#base_revisionObject



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/braid/mirror.rb', line 213

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



275
276
277
# File 'lib/braid/mirror.rb', line 275

def branch
  self.attributes['branch']
end

#branch=(new_value) ⇒ Object



280
281
282
# File 'lib/braid/mirror.rb', line 280

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

#cached?Boolean

Returns:

  • (Boolean)


208
209
210
# File 'lib/braid/mirror.rb', line 208

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

#cached_urlObject



317
318
319
# File 'lib/braid/mirror.rb', line 317

def cached_url
  git_cache.path(url)
end

#diffObject



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

def diff
  fetch_base_revision_if_missing
  git.diff(diff_args)
end

#diff_args(user_args = []) ⇒ Object



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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/braid/mirror.rb', line 129

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



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

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

#fetch_base_revision_if_missingObject



191
192
193
194
195
196
197
198
199
# File 'lib/braid/mirror.rb', line 191

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



238
239
240
241
242
243
# File 'lib/braid/mirror.rb', line 238

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)


106
107
108
# File 'lib/braid/mirror.rb', line 106

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

#merged?(commit) ⇒ Boolean

Returns:

  • (Boolean)


111
112
113
114
115
116
# File 'lib/braid/mirror.rb', line 111

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



322
323
324
325
326
327
328
329
# File 'lib/braid/mirror.rb', line 322

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



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

def remote_path
  self.attributes['path']
end

#remote_path=(remote_path) ⇒ Object



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

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

#remote_refObject



249
250
251
# File 'lib/braid/mirror.rb', line 249

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

#revisionObject



307
308
309
# File 'lib/braid/mirror.rb', line 307

def revision
  self.attributes['revision']
end

#revision=(new_value) ⇒ Object



312
313
314
# File 'lib/braid/mirror.rb', line 312

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

#tagObject



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

def tag
  self.attributes['tag']
end

#tag=(new_value) ⇒ Object



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

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

#upstream_item_for_revision(revision) ⇒ Object



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

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

#urlObject



265
266
267
# File 'lib/braid/mirror.rb', line 265

def url
  self.attributes['url']
end

#url=(new_value) ⇒ Object



270
271
272
# File 'lib/braid/mirror.rb', line 270

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