Merge lp:~jelmer/brz-git/diverged-branches into lp:brz-git

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 1912
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz-git/diverged-branches
Merge into: lp:brz-git
Diff against target: 358 lines (+118/-26)
8 files modified
branch.py (+14/-6)
fetch.py (+1/-1)
interrepo.py (+11/-7)
push.py (+31/-0)
refs.py (+15/-10)
remote.py (+12/-2)
tests/test_remote.py (+31/-0)
transportgit.py (+3/-0)
To merge this branch: bzr merge lp:~jelmer/brz-git/diverged-branches
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+342481@code.launchpad.net

Commit message

Check for diverged branches during push.

Description of the change

Check for diverged branches during push.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Rubberstamp! Proposer approves of own proposal.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'branch.py'
2--- branch.py 2018-03-26 22:28:24 +0000
3+++ branch.py 2018-03-31 18:16:06 +0000
4@@ -58,6 +58,9 @@
5 NoPushSupport,
6 NoSuchRef,
7 )
8+from .push import (
9+ remote_divergence,
10+ )
11 from .refs import (
12 is_tag,
13 ref_to_branch_name,
14@@ -943,17 +946,22 @@
15 isinstance(target, RemoteGitBranch))
16
17 def _basic_push(self, overwrite, stop_revision):
18- # TODO(jelmer): Support overwrite
19 result = GitBranchPushResult()
20 result.source_branch = self.source
21 result.target_branch = self.target
22 if stop_revision is None:
23 stop_revision = self.source.last_revision()
24- # TODO(jelmer): Check for diverged branches
25 def get_changed_refs(old_refs):
26- old_ref = old_refs.get(self.target.ref, ZERO_SHA)
27- result.old_revid = self.target.lookup_foreign_revision_id(old_ref)
28- refs = { self.target.ref: self.source.repository.lookup_bzr_revision_id(stop_revision)[0] }
29+ old_ref = old_refs.get(self.target.ref, None)
30+ if old_ref is None:
31+ result.old_revid = revision.NULL_REVISION
32+ else:
33+ result.old_revid = self.target.lookup_foreign_revision_id(old_ref)
34+ new_ref = self.source.repository.lookup_bzr_revision_id(stop_revision)[0]
35+ if not overwrite:
36+ if remote_divergence(old_ref, new_ref, self.source.repository._git.object_store):
37+ raise errors.DivergedBranches(self.source, self.target)
38+ refs = { self.target.ref: new_ref }
39 result.new_revid = stop_revision
40 for name, sha in self.source.repository._git.refs.as_dict("refs/tags").iteritems():
41 refs[tag_name_to_ref(name)] = sha
42@@ -1219,7 +1227,7 @@
43 return self._update_refs(result, old_refs, new_refs, overwrite)
44 try:
45 result.revidmap, old_refs, new_refs = self.interrepo.fetch_refs(
46- update_refs, lossy=lossy)
47+ update_refs, lossy=lossy, overwrite=overwrite)
48 except NoPushSupport:
49 raise errors.NoRoundtrippingSupport(self.source, self.target)
50 (old_sha1, result.old_revid) = old_refs.get(main_ref, (ZERO_SHA, NULL_REVISION))
51
52=== modified file 'fetch.py'
53--- fetch.py 2018-03-30 21:27:44 +0000
54+++ fetch.py 2018-03-31 18:16:06 +0000
55@@ -182,7 +182,7 @@
56
57 class SubmodulesRequireSubtrees(BzrError):
58 _fmt = ("The repository you are fetching from contains submodules, "
59- "which are not yet supported.")
60+ "which require a Bazaar format that supports tree references.")
61 internal = False
62
63
64
65=== modified file 'interrepo.py'
66--- interrepo.py 2018-03-31 11:45:36 +0000
67+++ interrepo.py 2018-03-31 18:16:06 +0000
68@@ -33,6 +33,7 @@
69 from dulwich.walk import Walker
70
71 from ...errors import (
72+ DivergedBranches,
73 FetchLimitUnsupported,
74 InvalidRevisionId,
75 LossyPushToSameVCS,
76@@ -67,6 +68,7 @@
77 )
78 from .push import (
79 MissingObjectsIterator,
80+ remote_divergence,
81 )
82 from .refs import (
83 is_tag,
84@@ -102,7 +104,7 @@
85 """See InterRepository.copy_content."""
86 self.fetch(revision_id, pb, find_ghosts=False)
87
88- def fetch_refs(self, update_refs, lossy):
89+ def fetch_refs(self, update_refs, lossy, overwrite=False):
90 """Fetch possibly roundtripped revisions into the target repository
91 and update refs.
92
93@@ -244,7 +246,7 @@
94 bzr_refs[k] = (v, revid)
95 return bzr_refs
96
97- def fetch_refs(self, update_refs, lossy):
98+ def fetch_refs(self, update_refs, lossy, overwrite=False):
99 self._warn_slow()
100 with self.source_store.lock_read():
101 old_refs = self._get_target_bzr_refs()
102@@ -321,7 +323,7 @@
103
104 class InterToRemoteGitRepository(InterToGitRepository):
105
106- def fetch_refs(self, update_refs, lossy):
107+ def fetch_refs(self, update_refs, lossy, overwrite=False):
108 """Import the gist of the ancestry of a particular revision."""
109 if not lossy and not self.mapping.roundtripping:
110 raise NoPushSupport(self.source, self.target, self.mapping)
111@@ -334,9 +336,11 @@
112 for name, (gitid, revid) in self.new_refs.iteritems():
113 if gitid is None:
114 git_sha = self.source_store._lookup_revision_sha1(revid)
115- ret[name] = unpeel_map.re_unpeel_tag(git_sha, old_refs.get(name))
116- else:
117- ret[name] = gitid
118+ gitid = unpeel_map.re_unpeel_tag(git_sha, old_refs.get(name))
119+ if not overwrite:
120+ if remote_divergence(old_refs.get(name), gitid, self.source_store):
121+ raise DivergedBranches(self.source, self.target)
122+ ret[name] = gitid
123 return ret
124 self._warn_slow()
125 with self.source_store.lock_read():
126@@ -597,7 +601,7 @@
127 class InterGitGitRepository(InterFromGitRepository):
128 """InterRepository that copies between Git repositories."""
129
130- def fetch_refs(self, update_refs, lossy):
131+ def fetch_refs(self, update_refs, lossy, overwrite=False):
132 if lossy:
133 raise LossyPushToSameVCS(self.source, self.target)
134 old_refs = self.target.controldir.get_refs_container()
135
136=== modified file 'push.py'
137--- push.py 2018-03-30 21:27:44 +0000
138+++ push.py 2018-03-31 18:16:06 +0000
139@@ -68,3 +68,34 @@
140
141 def __iter__(self):
142 return iter(self._pending)
143+
144+
145+class ObjectStoreParentsProvider(object):
146+
147+ def __init__(self, store):
148+ self._store = store
149+
150+ def get_parent_map(self, shas):
151+ ret = {}
152+ for sha in shas:
153+ if sha is None:
154+ parents = []
155+ else:
156+ try:
157+ parents = self._store[sha].parents
158+ except KeyError:
159+ parents = None
160+ ret[sha] = parents
161+ return ret
162+
163+
164+def remote_divergence(old_sha, new_sha, store):
165+ if old_sha is None:
166+ return False
167+ if not isinstance(old_sha, bytes):
168+ raise TypeError(old_sha)
169+ if not isinstance(new_sha, bytes):
170+ raise TypeError(new_sha)
171+ from breezy.graph import Graph
172+ graph = Graph(ObjectStoreParentsProvider(store))
173+ return not graph.is_ancestor(old_sha, new_sha)
174
175=== modified file 'refs.py'
176--- refs.py 2018-03-26 22:28:24 +0000
177+++ refs.py 2018-03-31 18:16:06 +0000
178@@ -18,6 +18,11 @@
179
180 from __future__ import absolute_import
181
182+from dulwich.refs import (
183+ ANNOTATED_TAG_SUFFIX,
184+ LOCAL_BRANCH_PREFIX,
185+ LOCAL_TAG_PREFIX,
186+ )
187 from dulwich.repo import (
188 RefsContainer,
189 )
190@@ -28,9 +33,9 @@
191 revision as _mod_revision,
192 )
193
194-is_tag = lambda x: x.startswith("refs/tags/")
195-is_head = lambda x: x.startswith("refs/heads/")
196-is_peeled = lambda x: x.endswith("^{}")
197+is_tag = lambda x: x.startswith(LOCAL_TAG_PREFIX)
198+is_head = lambda x: x.startswith(LOCAL_BRANCH_PREFIX)
199+is_peeled = lambda x: x.endswith(ANNOTATED_TAG_SUFFIX)
200
201
202 def gather_peeled(refs):
203@@ -39,7 +44,7 @@
204 if is_peeled(k):
205 continue
206 try:
207- peeled = refs[k+"^{}"]
208+ peeled = refs[k+ANNOTATED_TAG_SUFFIX]
209 unpeeled = v
210 except KeyError:
211 peeled = v
212@@ -57,7 +62,7 @@
213 if name == "":
214 return "HEAD"
215 if not name.startswith("refs/"):
216- return "refs/heads/%s" % osutils.safe_utf8(name)
217+ return LOCAL_BRANCH_PREFIX + osutils.safe_utf8(name)
218 else:
219 return osutils.safe_utf8(name)
220
221@@ -68,7 +73,7 @@
222 :param name: Tag name
223 :return: ref string
224 """
225- return "refs/tags/%s" % osutils.safe_utf8(name)
226+ return LOCAL_TAG_PREFIX + osutils.safe_utf8(name)
227
228
229 def ref_to_branch_name(ref):
230@@ -81,14 +86,14 @@
231 return u""
232 if ref is None:
233 return ref
234- if ref.startswith("refs/heads/"):
235- return osutils.safe_unicode(ref[len("refs/heads/"):])
236+ if ref.startswith(LOCAL_BRANCH_PREFIX):
237+ return osutils.safe_unicode(ref[len(LOCAL_BRANCH_PREFIX):])
238 raise ValueError("unable to map ref %s back to branch name" % ref)
239
240
241 def ref_to_tag_name(ref):
242- if ref.startswith("refs/tags/"):
243- return ref[len('refs/tags/'):].decode("utf-8")
244+ if ref.startswith(LOCAL_TAG_PREFIX):
245+ return ref[len(LOCAL_TAG_PREFIX):].decode("utf-8")
246 raise ValueError("unable to map ref %s back to tag name" % ref)
247
248
249
250=== modified file 'remote.py'
251--- remote.py 2018-03-31 13:08:14 +0000
252+++ remote.py 2018-03-31 18:16:06 +0000
253@@ -21,6 +21,7 @@
254 from ... import (
255 config,
256 debug,
257+ errors,
258 trace,
259 ui,
260 urlutils,
261@@ -31,6 +32,7 @@
262 from ...errors import (
263 AlreadyBranchError,
264 BzrError,
265+ DivergedBranches,
266 InProcessTransport,
267 InvalidRevisionId,
268 NoSuchFile,
269@@ -71,6 +73,9 @@
270 from .object_store import (
271 get_object_store,
272 )
273+from .push import (
274+ remote_divergence,
275+ )
276 from .repository import (
277 GitRepository,
278 )
279@@ -424,9 +429,14 @@
280 ret = dict(refs)
281 # TODO(jelmer): Unpeel if necessary
282 if lossy:
283- ret[refname] = source_store._lookup_revision_sha1(revision_id)
284+ new_sha = source_store._lookup_revision_sha1(revision_id)
285 else:
286- ret[refname] = repo.lookup_bzr_revision_id(revision_id)[0]
287+ new_sha = repo.lookup_bzr_revision_id(revision_id)[0]
288+ if not overwrite:
289+ if remote_divergence(ret.get(refname), new_sha, source_store):
290+ raise DivergedBranches(
291+ source, self.open_branch(name, nascent_ok=True))
292+ ret[refname] = new_sha
293 return ret
294 if lossy:
295 generate_pack_data = source_store.generate_lossy_pack_data
296
297=== modified file 'tests/test_remote.py'
298--- tests/test_remote.py 2018-03-31 13:08:14 +0000
299+++ tests/test_remote.py 2018-03-31 18:16:06 +0000
300@@ -24,6 +24,7 @@
301 from ....controldir import ControlDir
302 from ....errors import (
303 BzrError,
304+ DivergedBranches,
305 NotBranchError,
306 NoSuchTag,
307 )
308@@ -248,6 +249,36 @@
309 },
310 self.remote_real.get_refs())
311
312+ def test_push_diverged(self):
313+ c1 = self.remote_real.do_commit(
314+ message='message',
315+ committer='committer <committer@example.com>',
316+ author='author <author@example.com>',
317+ ref='refs/heads/newbranch')
318+
319+ remote = ControlDir.open(self.remote_url)
320+ wt = self.make_branch_and_tree('local', format=self._from_format)
321+ self.build_tree(['local/blah'])
322+ wt.add(['blah'])
323+ revid = wt.commit('blah')
324+
325+ newbranch = remote.open_branch('newbranch')
326+ if self._from_format == 'git':
327+ self.assertRaises(DivergedBranches, wt.branch.push, newbranch)
328+ else:
329+ self.assertRaises(DivergedBranches, wt.branch.push, newbranch, lossy=True)
330+
331+ self.assertEqual(
332+ {'refs/heads/newbranch': c1 },
333+ self.remote_real.get_refs())
334+
335+ if self._from_format == 'git':
336+ wt.branch.push(newbranch, overwrite=True)
337+ else:
338+ wt.branch.push(newbranch, lossy=True, overwrite=True)
339+
340+ self.assertNotEqual(c1, self.remote_real.refs['refs/heads/newbranch'])
341+
342
343 class PushToRemoteFromBzrTests(PushToRemoteBase,TestCaseWithTransport):
344
345
346=== modified file 'transportgit.py'
347--- transportgit.py 2018-03-28 21:22:43 +0000
348+++ transportgit.py 2018-03-31 18:16:06 +0000
349@@ -392,6 +392,9 @@
350 def controldir(self):
351 return self._controltransport.local_abspath('.')
352
353+ def commondir(self):
354+ return self._commontransport.local_abspath('.')
355+
356 @property
357 def path(self):
358 return self.transport.local_abspath('.')

Subscribers

People subscribed via source and target branches

to all changes: