/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/git/branch.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
"""An adapter between a Git Branch and a Bazaar Branch"""
19
19
 
20
 
from __future__ import absolute_import
21
20
 
 
21
import contextlib
22
22
from io import BytesIO
23
23
from collections import defaultdict
24
24
 
 
25
from dulwich.config import (
 
26
    ConfigFile as GitConfigFile,
 
27
    parse_submodules,
 
28
    )
 
29
 
25
30
from dulwich.objects import (
26
31
    NotCommitError,
27
32
    ZERO_SHA,
36
41
    lock,
37
42
    repository as _mod_repository,
38
43
    revision,
39
 
    tag,
40
44
    trace,
41
45
    transport,
42
46
    urlutils,
45
49
from ..revision import (
46
50
    NULL_REVISION,
47
51
    )
48
 
from ..sixish import (
49
 
    text_type,
50
 
    viewitems,
 
52
from ..tag import (
 
53
    Tags,
 
54
    InterTags,
51
55
    )
52
56
from ..trace import (
53
57
    is_quiet,
55
59
    warning,
56
60
    )
57
61
 
58
 
from .config import (
59
 
    GitBranchConfig,
60
 
    GitBranchStack,
61
 
    )
62
62
from .errors import (
63
63
    NoPushSupport,
64
64
    )
 
65
from .mapping import (
 
66
    encode_git_path,
 
67
    decode_git_path,
 
68
    )
65
69
from .push import (
66
70
    remote_divergence,
67
71
    )
76
80
from .unpeel_map import (
77
81
    UnpeelMap,
78
82
    )
79
 
from .urls import git_url_to_bzr_url
 
83
from .urls import (
 
84
    git_url_to_bzr_url,
 
85
    bzr_url_to_git_url,
 
86
    )
 
87
 
 
88
 
 
89
def _calculate_revnos(branch):
 
90
    if branch._format.stores_revno():
 
91
        return True
 
92
    config = branch.get_config_stack()
 
93
    return config.get('calculate_revnos')
80
94
 
81
95
 
82
96
class GitPullResult(branch.PullResult):
85
99
    def _lookup_revno(self, revid):
86
100
        if not isinstance(revid, bytes):
87
101
            raise TypeError(revid)
 
102
        if not _calculate_revnos(self.target_branch):
 
103
            return None
88
104
        # Try in source branch first, it'll be faster
89
105
        with self.target_branch.lock_read():
90
106
            return self.target_branch.revision_id_to_revno(revid)
98
114
        return self._lookup_revno(self.new_revid)
99
115
 
100
116
 
101
 
class GitTags(tag.BasicTags):
102
 
    """Ref-based tag dictionary."""
103
 
 
104
 
    def __init__(self, branch):
105
 
        self.branch = branch
106
 
        self.repository = branch.repository
107
 
 
108
 
    def _merge_to_remote_git(self, target_repo, source_tag_refs,
109
 
                             overwrite=False):
 
117
class InterTagsFromGitToRemoteGit(InterTags):
 
118
 
 
119
    @classmethod
 
120
    def is_compatible(klass, source, target):
 
121
        if not isinstance(source, GitTags):
 
122
            return False
 
123
        if not isinstance(target, GitTags):
 
124
            return False
 
125
        if getattr(target.branch.repository, "_git", None) is not None:
 
126
            return False
 
127
        return True
 
128
 
 
129
    def merge(self, overwrite=False, ignore_master=False, selector=None):
 
130
        if self.source.branch.repository.has_same_location(self.target.branch.repository):
 
131
            return {}, []
110
132
        updates = {}
111
133
        conflicts = []
 
134
        source_tag_refs = self.source.branch.get_tag_refs()
 
135
        ref_to_tag_map = {}
112
136
 
113
137
        def get_changed_refs(old_refs):
114
138
            ret = dict(old_refs)
115
139
            for ref_name, tag_name, peeled, unpeeled in (
116
140
                    source_tag_refs.iteritems()):
 
141
                if selector and not selector(tag_name):
 
142
                    continue
117
143
                if old_refs.get(ref_name) == unpeeled:
118
144
                    pass
119
145
                elif overwrite or ref_name not in old_refs:
120
146
                    ret[ref_name] = unpeeled
121
 
                    updates[tag_name] = target_repo.lookup_foreign_revision_id(
 
147
                    updates[tag_name] = self.target.branch.repository.lookup_foreign_revision_id(
122
148
                        peeled)
 
149
                    ref_to_tag_map[ref_name] = tag_name
 
150
                    self.target.branch._tag_refs = None
123
151
                else:
124
152
                    conflicts.append(
125
153
                        (tag_name,
126
154
                         self.repository.lookup_foreign_revision_id(peeled),
127
 
                         target_repo.lookup_foreign_revision_id(
 
155
                         self.target.branch.repository.lookup_foreign_revision_id(
128
156
                             old_refs[ref_name])))
129
157
            return ret
130
 
        target_repo.controldir.send_pack(
 
158
        result = self.target.branch.repository.controldir.send_pack(
131
159
            get_changed_refs, lambda have, want: [])
132
 
        return updates, conflicts
133
 
 
134
 
    def _merge_to_local_git(self, target_repo, source_tag_refs,
135
 
                            overwrite=False):
 
160
        if result is not None and not isinstance(result, dict):
 
161
            for ref, error in result.ref_status.items():
 
162
                if error:
 
163
                    warning('unable to update ref %s: %s',
 
164
                            ref, error)
 
165
                    del updates[ref_to_tag_map[ref]]
 
166
        return updates, set(conflicts)
 
167
 
 
168
 
 
169
class InterTagsFromGitToLocalGit(InterTags):
 
170
 
 
171
    @classmethod
 
172
    def is_compatible(klass, source, target):
 
173
        if not isinstance(source, GitTags):
 
174
            return False
 
175
        if not isinstance(target, GitTags):
 
176
            return False
 
177
        if getattr(target.branch.repository, "_git", None) is None:
 
178
            return False
 
179
        return True
 
180
 
 
181
    def merge(self, overwrite=False, ignore_master=False, selector=None):
 
182
        if self.source.branch.repository.has_same_location(self.target.branch.repository):
 
183
            return {}, []
 
184
 
136
185
        conflicts = []
137
186
        updates = {}
 
187
        source_tag_refs = self.source.branch.get_tag_refs()
 
188
 
 
189
        target_repo = self.target.branch.repository
 
190
 
138
191
        for ref_name, tag_name, peeled, unpeeled in source_tag_refs:
 
192
            if selector and not selector(tag_name):
 
193
                continue
139
194
            if target_repo._git.refs.get(ref_name) == unpeeled:
140
195
                pass
141
196
            elif overwrite or ref_name not in target_repo._git.refs:
 
197
                try:
 
198
                    updates[tag_name] = (
 
199
                        target_repo.lookup_foreign_revision_id(peeled))
 
200
                except KeyError:
 
201
                    trace.warning('%s does not point to a valid object',
 
202
                                  tag_name)
 
203
                    continue
 
204
                except NotCommitError:
 
205
                    trace.warning('%s points to a non-commit object',
 
206
                                  tag_name)
 
207
                    continue
142
208
                target_repo._git.refs[ref_name] = unpeeled or peeled
143
 
                try:
144
 
                    updates[tag_name] = (
145
 
                        self.repository.lookup_foreign_revision_id(peeled))
146
 
                except KeyError:
147
 
                    trace.warning('%s does not point to a valid object',
148
 
                                  tag_name)
149
 
                    continue
 
209
                self.target.branch._tag_refs = None
150
210
            else:
151
211
                try:
152
 
                    source_revid = self.repository.lookup_foreign_revision_id(
 
212
                    source_revid = self.source.branch.repository.lookup_foreign_revision_id(
153
213
                        peeled)
154
214
                    target_revid = target_repo.lookup_foreign_revision_id(
155
215
                        target_repo._git.refs[ref_name])
157
217
                    trace.warning('%s does not point to a valid object',
158
218
                                  ref_name)
159
219
                    continue
 
220
                except NotCommitError:
 
221
                    trace.warning('%s points to a non-commit object',
 
222
                                  tag_name)
 
223
                    continue
160
224
                conflicts.append((tag_name, source_revid, target_revid))
161
 
        return updates, conflicts
162
 
 
163
 
    def _merge_to_git(self, to_tags, source_tag_refs, overwrite=False):
164
 
        target_repo = to_tags.repository
165
 
        if self.repository.has_same_location(target_repo):
166
 
            return {}, []
167
 
        try:
168
 
            if getattr(target_repo, "_git", None):
169
 
                return self._merge_to_local_git(
170
 
                    target_repo, source_tag_refs, overwrite)
171
 
            else:
172
 
                return self._merge_to_remote_git(
173
 
                    target_repo, source_tag_refs, overwrite)
174
 
        finally:
175
 
            to_tags.branch._tag_refs = None
176
 
 
177
 
    def _merge_to_non_git(self, to_tags, source_tag_refs, overwrite=False):
 
225
        return updates, set(conflicts)
 
226
 
 
227
 
 
228
class InterTagsFromGitToNonGit(InterTags):
 
229
 
 
230
    @classmethod
 
231
    def is_compatible(klass, source, target):
 
232
        if not isinstance(source, GitTags):
 
233
            return False
 
234
        if isinstance(target, GitTags):
 
235
            return False
 
236
        return True
 
237
 
 
238
    def merge(self, overwrite=False, ignore_master=False, selector=None):
 
239
        """See Tags.merge_to."""
 
240
        source_tag_refs = self.source.branch.get_tag_refs()
 
241
        if ignore_master:
 
242
            master = None
 
243
        else:
 
244
            master = self.target.branch.get_master_branch()
 
245
        with contextlib.ExitStack() as es:
 
246
            if master is not None:
 
247
                es.enter_context(master.lock_write())
 
248
            updates, conflicts = self._merge_to(
 
249
                self.target, source_tag_refs, overwrite=overwrite,
 
250
                selector=selector)
 
251
            if master is not None:
 
252
                extra_updates, extra_conflicts = self._merge_to(
 
253
                    master.tags, overwrite=overwrite,
 
254
                    source_tag_refs=source_tag_refs,
 
255
                    ignore_master=ignore_master, selector=selector)
 
256
                updates.update(extra_updates)
 
257
                conflicts.update(extra_conflicts)
 
258
            return updates, conflicts
 
259
 
 
260
    def _merge_to(self, to_tags, source_tag_refs, overwrite=False,
 
261
                  selector=None):
178
262
        unpeeled_map = defaultdict(set)
179
263
        conflicts = []
180
264
        updates = {}
181
265
        result = dict(to_tags.get_tag_dict())
182
266
        for ref_name, tag_name, peeled, unpeeled in source_tag_refs:
 
267
            if selector and not selector(tag_name):
 
268
                continue
183
269
            if unpeeled is not None:
184
270
                unpeeled_map[peeled].add(unpeeled)
185
271
            try:
186
 
                bzr_revid = self.branch.lookup_foreign_revision_id(peeled)
 
272
                bzr_revid = self.source.branch.lookup_foreign_revision_id(peeled)
187
273
            except NotCommitError:
188
274
                continue
189
275
            if result.get(tag_name) == bzr_revid:
198
284
            map_file = UnpeelMap.from_repository(to_tags.branch.repository)
199
285
            map_file.update(unpeeled_map)
200
286
            map_file.save_in_repository(to_tags.branch.repository)
201
 
        return updates, conflicts
202
 
 
203
 
    def merge_to(self, to_tags, overwrite=False, ignore_master=False,
204
 
                 source_tag_refs=None):
205
 
        """See Tags.merge_to."""
206
 
        if source_tag_refs is None:
207
 
            source_tag_refs = self.branch.get_tag_refs()
208
 
        if self == to_tags:
209
 
            return {}, []
210
 
        if isinstance(to_tags, GitTags):
211
 
            return self._merge_to_git(to_tags, source_tag_refs,
212
 
                                      overwrite=overwrite)
213
 
        else:
214
 
            if ignore_master:
215
 
                master = None
216
 
            else:
217
 
                master = to_tags.branch.get_master_branch()
218
 
            if master is not None:
219
 
                master.lock_write()
220
 
            try:
221
 
                updates, conflicts = self._merge_to_non_git(
222
 
                    to_tags, source_tag_refs, overwrite=overwrite)
223
 
                if master is not None:
224
 
                    extra_updates, extra_conflicts = self.merge_to(
225
 
                        master.tags, overwrite=overwrite,
226
 
                        source_tag_refs=source_tag_refs,
227
 
                        ignore_master=ignore_master)
228
 
                    updates.update(extra_updates)
229
 
                    conflicts += extra_conflicts
230
 
                return updates, conflicts
231
 
            finally:
232
 
                if master is not None:
233
 
                    master.unlock()
 
287
        return updates, set(conflicts)
 
288
 
 
289
 
 
290
InterTags.register_optimiser(InterTagsFromGitToRemoteGit)
 
291
InterTags.register_optimiser(InterTagsFromGitToLocalGit)
 
292
InterTags.register_optimiser(InterTagsFromGitToNonGit)
 
293
 
 
294
 
 
295
class GitTags(Tags):
 
296
    """Ref-based tag dictionary."""
 
297
 
 
298
    def __init__(self, branch):
 
299
        self.branch = branch
 
300
        self.repository = branch.repository
234
301
 
235
302
    def get_tag_dict(self):
236
303
        ret = {}
244
311
                ret[tag_name] = bzr_revid
245
312
        return ret
246
313
 
 
314
    def lookup_tag(self, tag_name):
 
315
        """Return the referent string of a tag"""
 
316
        # TODO(jelmer): Replace with something more efficient for local tags.
 
317
        td = self.get_tag_dict()
 
318
        try:
 
319
            return td[tag_name]
 
320
        except KeyError:
 
321
            raise errors.NoSuchTag(tag_name)
 
322
 
247
323
 
248
324
class LocalGitTagDict(GitTags):
249
325
    """Dictionary with tags in a local repository."""
254
330
 
255
331
    def _set_tag_dict(self, to_dict):
256
332
        extra = set(self.refs.allkeys())
257
 
        for k, revid in viewitems(to_dict):
 
333
        for k, revid in to_dict.items():
258
334
            name = tag_name_to_ref(k)
259
335
            if name in extra:
260
336
                extra.remove(name)
261
 
            self.set_tag(k, revid)
 
337
            try:
 
338
                self.set_tag(k, revid)
 
339
            except errors.GhostTagsNotSupported:
 
340
                pass
262
341
        for name in extra:
263
342
            if is_tag(name):
264
343
                del self.repository._git[name]
321
400
    def set_reference(self, controldir, name, target):
322
401
        return controldir.set_branch_reference(target, name)
323
402
 
 
403
    def stores_revno(self):
 
404
        """True if this branch format store revision numbers."""
 
405
        return False
 
406
 
 
407
    supports_reference_locations = False
 
408
 
324
409
 
325
410
class LocalGitBranchFormat(GitBranchFormat):
326
411
 
397
482
        return "git"
398
483
 
399
484
    def get_config(self):
 
485
        from .config import GitBranchConfig
400
486
        return GitBranchConfig(self)
401
487
 
402
488
    def get_config_stack(self):
 
489
        from .config import GitBranchStack
403
490
        return GitBranchStack(self)
404
491
 
405
492
    def _get_nick(self, local=False, possible_master_transports=None):
411
498
            cs = self.repository._git.get_config_stack()
412
499
            try:
413
500
                return cs.get((b"branch", self.name.encode('utf-8')),
414
 
                              b"nick").decode("utf-8")
 
501
                        b"nick").decode("utf-8")
415
502
            except KeyError:
416
503
                pass
417
504
        return self.name or u"HEAD"
464
551
        # Git doesn't do stacking (yet...)
465
552
        raise branch.UnstackableBranchFormat(self._format, self.base)
466
553
 
 
554
    def _get_push_origin(self, cs):
 
555
        """Get the name for the push origin.
 
556
 
 
557
        The exact behaviour is documented in the git-config(1) manpage.
 
558
        """
 
559
        try:
 
560
            return cs.get((b'branch', self.name.encode('utf-8')), b'pushRemote')
 
561
        except KeyError:
 
562
            try:
 
563
                return cs.get((b'branch', ), b'remote')
 
564
            except KeyError:
 
565
                try:
 
566
                    return cs.get((b'branch', self.name.encode('utf-8')), b'remote')
 
567
                except KeyError:
 
568
                    return b'origin'
 
569
 
 
570
    def _get_origin(self, cs):
 
571
        try:
 
572
            return cs.get((b'branch', self.name.encode('utf-8')), b'remote')
 
573
        except KeyError:
 
574
            return b'origin'
 
575
 
 
576
    def _get_related_push_branch(self, cs):
 
577
        remote = self._get_push_origin(cs)
 
578
        try:
 
579
            location = cs.get((b"remote", remote), b"url")
 
580
        except KeyError:
 
581
            return None
 
582
 
 
583
        return git_url_to_bzr_url(location.decode('utf-8'), ref=self.ref)
 
584
 
 
585
    def _get_related_merge_branch(self, cs):
 
586
        remote = self._get_origin(cs)
 
587
        try:
 
588
            location = cs.get((b"remote", remote), b"url")
 
589
        except KeyError:
 
590
            return None
 
591
 
 
592
        try:
 
593
            ref = cs.get((b"branch", remote), b"merge")
 
594
        except KeyError:
 
595
            ref = self.ref
 
596
 
 
597
        return git_url_to_bzr_url(location.decode('utf-8'), ref=ref)
 
598
 
467
599
    def _get_parent_location(self):
468
600
        """See Branch.get_parent()."""
469
 
        # FIXME: Set "origin" url from .git/config ?
470
601
        cs = self.repository._git.get_config_stack()
471
 
        try:
472
 
            location = cs.get((b"remote", b'origin'), b"url")
473
 
        except KeyError:
474
 
            return None
475
 
 
476
 
        params = {}
477
 
        try:
478
 
            ref = cs.get((b"remote", b"origin"), b"merge")
479
 
        except KeyError:
480
 
            pass
481
 
        else:
482
 
            if ref != b'HEAD':
483
 
                try:
484
 
                    params['branch'] = urlutils.escape(ref_to_branch_name(ref))
485
 
                except ValueError:
486
 
                    params['ref'] = urlutils.quote_from_bytes(ref)
487
 
 
488
 
        url = git_url_to_bzr_url(location.decode('utf-8'))
489
 
        return urlutils.join_segment_parameters(url, params)
 
602
        return self._get_related_merge_branch(cs)
 
603
 
 
604
    def _write_git_config(self, cs):
 
605
        f = BytesIO()
 
606
        cs.write_to_file(f)
 
607
        self.repository._git._put_named_file('config', f.getvalue())
490
608
 
491
609
    def set_parent(self, location):
492
 
        # FIXME: Set "origin" url in .git/config ?
493
610
        cs = self.repository._git.get_config()
494
 
        this_url = urlutils.split_segment_parameters(self.user_url)[0]
495
 
        target_url, target_params = urlutils.split_segment_parameters(location)
 
611
        remote = self._get_origin(cs)
 
612
        this_url = urlutils.strip_segment_parameters(self.user_url)
 
613
        target_url, branch, ref = bzr_url_to_git_url(location)
496
614
        location = urlutils.relative_url(this_url, target_url)
497
 
        cs.set((b"remote", b"origin"), b"url", location)
498
 
        if 'branch' in target_params:
499
 
            cs.set((b"remote", b"origin"), b"merge",
500
 
                   branch_name_to_ref(target_params['branch']))
501
 
        elif 'ref' in target_params:
502
 
            cs.set((b"remote", b"origin"), b"merge",
503
 
                   target_params['ref'])
 
615
        cs.set((b"remote", remote), b"url", location)
 
616
        if branch:
 
617
            cs.set((b"branch", remote), b"merge", branch_name_to_ref(branch))
 
618
        elif ref:
 
619
            cs.set((b"branch", remote), b"merge", ref)
504
620
        else:
505
621
            # TODO(jelmer): Maybe unset rather than setting to HEAD?
506
 
            cs.set((b"remote", b"origin"), b"merge", 'HEAD')
507
 
        f = BytesIO()
508
 
        cs.write_to_file(f)
509
 
        self.repository._git._put_named_file('config', f.getvalue())
 
622
            cs.set((b"branch", remote), b"merge", b'HEAD')
 
623
        self._write_git_config(cs)
510
624
 
511
625
    def break_lock(self):
512
626
        raise NotImplementedError(self.break_lock)
558
672
                return revision.NULL_REVISION
559
673
            return self.lookup_foreign_revision_id(self.head)
560
674
 
561
 
    def _basic_push(self, target, overwrite=False, stop_revision=None):
 
675
    def _basic_push(self, target, overwrite=False, stop_revision=None,
 
676
                    tag_selector=None):
562
677
        return branch.InterBranch.get(self, target)._basic_push(
563
 
            overwrite, stop_revision)
 
678
            overwrite, stop_revision, tag_selector=tag_selector)
564
679
 
565
680
    def lookup_foreign_revision_id(self, foreign_revid):
566
681
        try:
615
730
            self, stop_revision=revid, lossy=lossy, _stop_revno=revno)
616
731
        return (push_result.new_revno, push_result.new_revid)
617
732
 
 
733
    def reconcile(self, thorough=True):
 
734
        """Make sure the data stored in this branch is consistent."""
 
735
        from ..reconcile import ReconcileResult
 
736
        # Nothing to do here
 
737
        return ReconcileResult()
 
738
 
618
739
 
619
740
class LocalGitBranch(GitBranch):
620
741
    """A local Git branch."""
650
771
    def break_lock(self):
651
772
        self.repository._git.refs.unlock_ref(self.ref)
652
773
 
653
 
    def fetch(self, from_branch, last_revision=None, limit=None):
654
 
        return branch.InterBranch.get(from_branch, self).fetch(
655
 
            stop_revision=last_revision, limit=limit)
656
 
 
657
774
    def _gen_revision_history(self):
658
775
        if self.head is None:
659
776
            return []
714
831
    def get_push_location(self):
715
832
        """See Branch.get_push_location."""
716
833
        push_loc = self.get_config_stack().get('push_location')
717
 
        return push_loc
 
834
        if push_loc is not None:
 
835
            return push_loc
 
836
        cs = self.repository._git.get_config_stack()
 
837
        return self._get_related_push_branch(cs)
718
838
 
719
839
    def set_push_location(self, location):
720
840
        """See Branch.set_push_location."""
733
853
        :param refs: Refs dictionary (name -> git sha1)
734
854
        :return: iterator over (ref_name, tag_name, peeled_sha1, unpeeled_sha1)
735
855
        """
736
 
        refs = self.repository._git.refs
737
 
        for ref_name, unpeeled in viewitems(refs.as_dict()):
 
856
        refs = self.repository.controldir.get_refs_container()
 
857
        for ref_name, unpeeled in refs.as_dict().items():
738
858
            try:
739
859
                tag_name = ref_to_tag_name(ref_name)
740
860
            except (ValueError, UnicodeDecodeError):
742
862
            peeled = refs.get_peeled(ref_name)
743
863
            if peeled is None:
744
864
                peeled = unpeeled
745
 
            if not isinstance(tag_name, text_type):
 
865
            if not isinstance(tag_name, str):
746
866
                raise TypeError(tag_name)
747
867
            yield (ref_name, tag_name, peeled, unpeeled)
748
868
 
751
871
        return GitMemoryTree(self, self.repository._git.object_store,
752
872
                             self.head)
753
873
 
754
 
    def reference_parent(self, path, file_id=None, possible_transports=None):
755
 
        """Return the parent branch for a tree-reference file_id
756
 
 
757
 
        :param path: The path of the file_id in the tree
758
 
        :param file_id: Optional file_id of the tree reference
759
 
        :return: A branch associated with the file_id
760
 
        """
761
 
        # FIXME should provide multiple branches, based on config
762
 
        url = urlutils.join(self.user_url, path)
763
 
        return branch.Branch.open(
764
 
            url,
765
 
            possible_transports=possible_transports)
766
 
 
767
874
 
768
875
def _quick_lookup_revno(local_branch, remote_branch, revid):
769
876
    if not isinstance(revid, bytes):
770
877
        raise TypeError(revid)
771
878
    # Try in source branch first, it'll be faster
772
879
    with local_branch.lock_read():
 
880
        if not _calculate_revnos(local_branch):
 
881
            return None
773
882
        try:
774
883
            return local_branch.revision_id_to_revno(revid)
775
884
        except errors.NoSuchRevision:
778
887
                return graph.find_distance_to_null(
779
888
                    revid, [(revision.NULL_REVISION, 0)])
780
889
            except errors.GhostRevisionsHaveNoRevno:
 
890
                if not _calculate_revnos(remote_branch):
 
891
                    return None
781
892
                # FIXME: Check using graph.find_distance_to_null() ?
782
893
                with remote_branch.lock_read():
783
894
                    return remote_branch.revision_id_to_revno(revid)
879
990
            return False
880
991
        return True
881
992
 
882
 
    def fetch(self, stop_revision=None, fetch_tags=None, limit=None):
883
 
        self.fetch_objects(stop_revision, fetch_tags=fetch_tags, limit=limit)
 
993
    def fetch(self, stop_revision=None, fetch_tags=None, limit=None, lossy=False):
 
994
        self.fetch_objects(
 
995
            stop_revision, fetch_tags=fetch_tags, limit=limit, lossy=lossy)
 
996
        return _mod_repository.FetchResult()
884
997
 
885
 
    def fetch_objects(self, stop_revision, fetch_tags, limit=None):
 
998
    def fetch_objects(self, stop_revision, fetch_tags, limit=None, lossy=False, tag_selector=None):
886
999
        interrepo = self._get_interrepo(self.source, self.target)
887
1000
        if fetch_tags is None:
888
1001
            c = self.source.get_config_stack()
900
1013
            else:
901
1014
                self._last_revid = stop_revision
902
1015
            real = interrepo.get_determine_wants_revids(
903
 
                [self._last_revid], include_tags=fetch_tags)
 
1016
                [self._last_revid], include_tags=fetch_tags, tag_selector=tag_selector)
904
1017
            return real(heads)
905
1018
        pack_hint, head, refs = interrepo.fetch_objects(
906
 
            determine_wants, self.source.mapping, limit=limit)
 
1019
            determine_wants, self.source.mapping, limit=limit,
 
1020
            lossy=lossy)
907
1021
        if (pack_hint is not None and
908
1022
                self.target.repository._format.pack_compresses):
909
1023
            self.target.repository.pack(hint=pack_hint)
910
1024
        return head, refs
911
1025
 
912
 
    def _update_revisions(self, stop_revision=None, overwrite=False):
913
 
        head, refs = self.fetch_objects(stop_revision, fetch_tags=None)
 
1026
    def _update_revisions(self, stop_revision=None, overwrite=False, tag_selector=None):
 
1027
        head, refs = self.fetch_objects(stop_revision, fetch_tags=None, tag_selector=tag_selector)
914
1028
        if overwrite:
915
1029
            prev_last_revid = None
916
1030
        else:
920
1034
            other_branch=self.source)
921
1035
        return head, refs
922
1036
 
 
1037
    def update_references(self, revid=None):
 
1038
        if revid is None:
 
1039
            revid = self.target.last_revision()
 
1040
        tree = self.target.repository.revision_tree(revid)
 
1041
        try:
 
1042
            with tree.get_file('.gitmodules') as f:
 
1043
                for path, url, section in parse_submodules(
 
1044
                        GitConfigFile.from_file(f)):
 
1045
                    self.target.set_reference_info(
 
1046
                        tree.path2id(decode_git_path(path)), url.decode('utf-8'),
 
1047
                        decode_git_path(path))
 
1048
        except errors.NoSuchFile:
 
1049
            pass
 
1050
 
923
1051
    def _basic_pull(self, stop_revision, overwrite, run_hooks,
924
 
                    _override_hook_target, _hook_master):
 
1052
                    _override_hook_target, _hook_master, tag_selector=None):
925
1053
        if overwrite is True:
926
1054
            overwrite = set(["history", "tags"])
927
1055
        elif not overwrite:
938
1066
            (result.old_revno, result.old_revid) = \
939
1067
                self.target.last_revision_info()
940
1068
            result.new_git_head, remote_refs = self._update_revisions(
941
 
                stop_revision, overwrite=("history" in overwrite))
 
1069
                stop_revision, overwrite=("history" in overwrite),
 
1070
                tag_selector=tag_selector)
942
1071
            tags_ret = self.source.tags.merge_to(
943
1072
                self.target.tags, ("tags" in overwrite), ignore_master=True)
944
1073
            if isinstance(tags_ret, tuple):
947
1076
                result.tag_conflicts = tags_ret
948
1077
            (result.new_revno, result.new_revid) = \
949
1078
                self.target.last_revision_info()
 
1079
            self.update_references(revid=result.new_revid)
950
1080
            if _hook_master:
951
1081
                result.master_branch = _hook_master
952
1082
                result.local_branch = result.target_branch
960
1090
 
961
1091
    def pull(self, overwrite=False, stop_revision=None,
962
1092
             possible_transports=None, _hook_master=None, run_hooks=True,
963
 
             _override_hook_target=None, local=False):
 
1093
             _override_hook_target=None, local=False, tag_selector=None):
964
1094
        """See Branch.pull.
965
1095
 
966
1096
        :param _hook_master: Private parameter - set the branch to
975
1105
        bound_location = self.target.get_bound_location()
976
1106
        if local and not bound_location:
977
1107
            raise errors.LocalRequiresBoundBranch()
978
 
        master_branch = None
979
1108
        source_is_master = False
980
 
        self.source.lock_read()
981
 
        if bound_location:
982
 
            # bound_location comes from a config file, some care has to be
983
 
            # taken to relate it to source.user_url
984
 
            normalized = urlutils.normalize_url(bound_location)
985
 
            try:
986
 
                relpath = self.source.user_transport.relpath(normalized)
987
 
                source_is_master = (relpath == '')
988
 
            except (errors.PathNotChild, urlutils.InvalidURL):
989
 
                source_is_master = False
990
 
        if not local and bound_location and not source_is_master:
991
 
            # not pulling from master, so we need to update master.
992
 
            master_branch = self.target.get_master_branch(possible_transports)
993
 
            master_branch.lock_write()
994
 
        try:
995
 
            try:
996
 
                if master_branch:
997
 
                    # pull from source into master.
998
 
                    master_branch.pull(self.source, overwrite, stop_revision,
999
 
                                       run_hooks=False)
1000
 
                result = self._basic_pull(stop_revision, overwrite, run_hooks,
1001
 
                                          _override_hook_target,
1002
 
                                          _hook_master=master_branch)
1003
 
            finally:
1004
 
                self.source.unlock()
1005
 
        finally:
1006
 
            if master_branch:
1007
 
                master_branch.unlock()
1008
 
        return result
 
1109
        with contextlib.ExitStack() as es:
 
1110
            es.enter_context(self.source.lock_read())
 
1111
            if bound_location:
 
1112
                # bound_location comes from a config file, some care has to be
 
1113
                # taken to relate it to source.user_url
 
1114
                normalized = urlutils.normalize_url(bound_location)
 
1115
                try:
 
1116
                    relpath = self.source.user_transport.relpath(normalized)
 
1117
                    source_is_master = (relpath == '')
 
1118
                except (errors.PathNotChild, urlutils.InvalidURL):
 
1119
                    source_is_master = False
 
1120
            if not local and bound_location and not source_is_master:
 
1121
                # not pulling from master, so we need to update master.
 
1122
                master_branch = self.target.get_master_branch(possible_transports)
 
1123
                es.enter_context(master_branch.lock_write())
 
1124
                # pull from source into master.
 
1125
                master_branch.pull(self.source, overwrite, stop_revision,
 
1126
                                   run_hooks=False)
 
1127
            else:
 
1128
                master_branch = None
 
1129
            return self._basic_pull(stop_revision, overwrite, run_hooks,
 
1130
                                    _override_hook_target,
 
1131
                                    _hook_master=master_branch,
 
1132
                                    tag_selector=tag_selector)
1009
1133
 
1010
 
    def _basic_push(self, overwrite, stop_revision):
 
1134
    def _basic_push(self, overwrite, stop_revision, tag_selector=None):
1011
1135
        if overwrite is True:
1012
1136
            overwrite = set(["history", "tags"])
1013
1137
        elif not overwrite:
1017
1141
        result.target_branch = self.target
1018
1142
        result.old_revno, result.old_revid = self.target.last_revision_info()
1019
1143
        result.new_git_head, remote_refs = self._update_revisions(
1020
 
            stop_revision, overwrite=("history" in overwrite))
 
1144
            stop_revision, overwrite=("history" in overwrite),
 
1145
            tag_selector=tag_selector)
1021
1146
        tags_ret = self.source.tags.merge_to(
1022
 
            self.target.tags, "tags" in overwrite, ignore_master=True)
 
1147
            self.target.tags, "tags" in overwrite, ignore_master=True,
 
1148
            selector=tag_selector)
1023
1149
        (result.tag_updates, result.tag_conflicts) = tags_ret
1024
1150
        result.new_revno, result.new_revid = self.target.last_revision_info()
 
1151
        self.update_references(revid=result.new_revid)
1025
1152
        return result
1026
1153
 
1027
1154
 
1028
1155
class InterGitBranch(branch.GenericInterBranch):
1029
1156
    """InterBranch implementation that pulls between Git branches."""
1030
1157
 
1031
 
    def fetch(self, stop_revision=None, fetch_tags=None, limit=None):
 
1158
    def fetch(self, stop_revision=None, fetch_tags=None, limit=None, lossy=False):
1032
1159
        raise NotImplementedError(self.fetch)
1033
1160
 
1034
1161
 
1047
1174
        return (isinstance(source, LocalGitBranch) and
1048
1175
                isinstance(target, RemoteGitBranch))
1049
1176
 
1050
 
    def _basic_push(self, overwrite, stop_revision):
 
1177
    def _basic_push(self, overwrite, stop_revision, tag_selector=None):
 
1178
        from .remote import parse_git_error
1051
1179
        result = GitBranchPushResult()
1052
1180
        result.source_branch = self.source
1053
1181
        result.target_branch = self.target
1070
1198
                    raise errors.DivergedBranches(self.source, self.target)
1071
1199
            refs = {self.target.ref: new_ref}
1072
1200
            result.new_revid = stop_revision
1073
 
            for name, sha in viewitems(
1074
 
                    self.source.repository._git.refs.as_dict(b"refs/tags")):
 
1201
            for name, sha in (
 
1202
                    self.source.repository._git.refs.as_dict(b"refs/tags").items()):
 
1203
                if tag_selector and not tag_selector(name):
 
1204
                    continue
 
1205
                if sha not in self.source.repository._git:
 
1206
                    trace.mutter('Ignoring missing SHA: %s', sha)
 
1207
                    continue
1075
1208
                refs[tag_name_to_ref(name)] = sha
1076
1209
            return refs
1077
 
        self.target.repository.send_pack(
 
1210
        dw_result = self.target.repository.send_pack(
1078
1211
            get_changed_refs,
1079
 
            self.source.repository._git.object_store.generate_pack_data)
 
1212
            self.source.repository._git.generate_pack_data)
 
1213
        if dw_result is not None and not isinstance(dw_result, dict):
 
1214
            error = dw_result.ref_status.get(self.target.ref)
 
1215
            if error:
 
1216
                raise parse_git_error(self.target.user_url, error)
 
1217
            for ref, error in dw_result.ref_status.items():
 
1218
                if error:
 
1219
                    trace.warning('unable to open ref %s: %s', ref, error)
1080
1220
        return result
1081
1221
 
1082
1222
 
1095
1235
        return (isinstance(source, GitBranch) and
1096
1236
                isinstance(target, LocalGitBranch))
1097
1237
 
1098
 
    def fetch(self, stop_revision=None, fetch_tags=None, limit=None):
1099
 
        interrepo = _mod_repository.InterRepository.get(self.source.repository,
1100
 
                                                        self.target.repository)
 
1238
    def fetch(self, stop_revision=None, fetch_tags=None, limit=None, lossy=False):
 
1239
        interrepo = _mod_repository.InterRepository.get(
 
1240
            self.source.repository, self.target.repository)
1101
1241
        if stop_revision is None:
1102
1242
            stop_revision = self.source.last_revision()
 
1243
        if fetch_tags is None:
 
1244
            c = self.source.get_config_stack()
 
1245
            fetch_tags = c.get('branch.fetch_tags')
1103
1246
        determine_wants = interrepo.get_determine_wants_revids(
1104
1247
            [stop_revision], include_tags=fetch_tags)
1105
 
        interrepo.fetch_objects(determine_wants, limit=limit)
 
1248
        interrepo.fetch_objects(determine_wants, limit=limit, lossy=lossy)
 
1249
        return _mod_repository.FetchResult()
1106
1250
 
1107
 
    def _basic_push(self, overwrite=False, stop_revision=None):
 
1251
    def _basic_push(self, overwrite=False, stop_revision=None, tag_selector=None):
1108
1252
        if overwrite is True:
1109
1253
            overwrite = set(["history", "tags"])
1110
1254
        elif not overwrite:
1120
1264
            other_branch=self.source)
1121
1265
        tags_ret = self.source.tags.merge_to(
1122
1266
            self.target.tags,
1123
 
            source_tag_refs=remote_refs_dict_to_tag_refs(refs),
1124
 
            overwrite=("tags" in overwrite))
 
1267
            overwrite=("tags" in overwrite),
 
1268
            selector=tag_selector)
1125
1269
        if isinstance(tags_ret, tuple):
1126
1270
            (result.tag_updates, result.tag_conflicts) = tags_ret
1127
1271
        else:
1136
1280
        fetch_tags = c.get('branch.fetch_tags')
1137
1281
 
1138
1282
        if stop_revision is None:
1139
 
            refs = interrepo.fetch(branches=[self.source.ref], include_tags=fetch_tags)
 
1283
            result = interrepo.fetch(branches=[self.source.ref], include_tags=fetch_tags)
1140
1284
            try:
1141
 
                head = refs[self.source.ref]
 
1285
                head = result.refs[self.source.ref]
1142
1286
            except KeyError:
1143
1287
                stop_revision = revision.NULL_REVISION
1144
1288
            else:
1145
1289
                stop_revision = self.target.lookup_foreign_revision_id(head)
1146
1290
        else:
1147
 
            refs = interrepo.fetch(
 
1291
            result = interrepo.fetch(
1148
1292
                revision_id=stop_revision, include_tags=fetch_tags)
1149
 
        return refs, stop_revision
 
1293
        return result.refs, stop_revision
1150
1294
 
1151
1295
    def pull(self, stop_revision=None, overwrite=False,
1152
 
             possible_transports=None, run_hooks=True, local=False):
 
1296
             possible_transports=None, run_hooks=True, local=False,
 
1297
             tag_selector=None):
1153
1298
        # This type of branch can't be bound.
1154
1299
        if local:
1155
1300
            raise errors.LocalRequiresBoundBranch()
1170
1315
                other_branch=self.source)
1171
1316
            tags_ret = self.source.tags.merge_to(
1172
1317
                self.target.tags, overwrite=("tags" in overwrite),
1173
 
                source_tag_refs=remote_refs_dict_to_tag_refs(refs))
 
1318
                selector=tag_selector)
1174
1319
            if isinstance(tags_ret, tuple):
1175
1320
                (result.tag_updates, result.tag_conflicts) = tags_ret
1176
1321
            else:
1215
1360
        if stop_revision is None:
1216
1361
            (stop_revno, stop_revision) = self.source.last_revision_info()
1217
1362
        elif stop_revno is None:
1218
 
            stop_revno = self.source.revision_id_to_revno(stop_revision)
 
1363
            try:
 
1364
                stop_revno = self.source.revision_id_to_revno(stop_revision)
 
1365
            except errors.NoSuchRevision:
 
1366
                stop_revno = None
1219
1367
        if not isinstance(stop_revision, bytes):
1220
1368
            raise TypeError(stop_revision)
1221
1369
        main_ref = self.target.ref
1223
1371
        if fetch_tags is None:
1224
1372
            c = self.source.get_config_stack()
1225
1373
            fetch_tags = c.get('branch.fetch_tags')
1226
 
        for name, revid in viewitems(self.source.tags.get_tag_dict()):
 
1374
        for name, revid in self.source.tags.get_tag_dict().items():
1227
1375
            if self.source.repository.has_revision(revid):
1228
1376
                ref = tag_name_to_ref(name)
1229
1377
                if not check_ref_format(ref):
1235
1383
                    refs[ref] = (None, revid)
1236
1384
        return refs, main_ref, (stop_revno, stop_revision)
1237
1385
 
1238
 
    def _update_refs(self, result, old_refs, new_refs, overwrite):
 
1386
    def _update_refs(self, result, old_refs, new_refs, overwrite, tag_selector):
1239
1387
        mutter("updating refs. old refs: %r, new refs: %r",
1240
1388
               old_refs, new_refs)
1241
1389
        result.tag_updates = {}
1242
1390
        result.tag_conflicts = []
1243
 
        ret = dict(old_refs)
 
1391
        ret = {}
1244
1392
 
1245
1393
        def ref_equals(refs, ref, git_sha, revid):
1246
1394
            try:
1260
1408
            # updated that hasn't actually been updated.
1261
1409
            return False
1262
1410
        # FIXME: Check for diverged branches
1263
 
        for ref, (git_sha, revid) in viewitems(new_refs):
 
1411
        for ref, (git_sha, revid) in new_refs.items():
1264
1412
            if ref_equals(ret, ref, git_sha, revid):
1265
1413
                # Already up to date
1266
1414
                if git_sha is None:
1274
1422
                except ValueError:
1275
1423
                    pass
1276
1424
                else:
 
1425
                    if tag_selector and not tag_selector(tag_name):
 
1426
                        continue
1277
1427
                    result.tag_updates[tag_name] = revid
1278
1428
                ret[ref] = (git_sha, revid)
1279
1429
            else:
1297
1447
            stop_revision = self.source.last_revision()
1298
1448
        ret = []
1299
1449
        if fetch_tags:
1300
 
            for k, v in viewitems(self.source.tags.get_tag_dict()):
 
1450
            for k, v in self.source.tags.get_tag_dict().items():
1301
1451
                ret.append((None, v))
1302
1452
        ret.append((None, stop_revision))
1303
1453
        try:
1304
 
            self.interrepo.fetch_objects(ret, lossy=lossy, limit=limit)
 
1454
            revidmap = self.interrepo.fetch_objects(ret, lossy=lossy, limit=limit)
1305
1455
        except NoPushSupport:
1306
1456
            raise errors.NoRoundtrippingSupport(self.source, self.target)
 
1457
        return _mod_repository.FetchResult(revidmap={
 
1458
            old_revid: new_revid
 
1459
            for (old_revid, (new_sha, new_revid)) in revidmap.items()})
1307
1460
 
1308
1461
    def pull(self, overwrite=False, stop_revision=None, local=False,
1309
 
             possible_transports=None, run_hooks=True, _stop_revno=None):
 
1462
             possible_transports=None, run_hooks=True, _stop_revno=None,
 
1463
             tag_selector=None):
1310
1464
        result = GitBranchPullResult()
1311
1465
        result.source_branch = self.source
1312
1466
        result.target_branch = self.target
1315
1469
                stop_revision, stop_revno=_stop_revno)
1316
1470
 
1317
1471
            def update_refs(old_refs):
1318
 
                return self._update_refs(result, old_refs, new_refs, overwrite)
 
1472
                return self._update_refs(result, old_refs, new_refs, overwrite, tag_selector)
1319
1473
            try:
1320
1474
                result.revidmap, old_refs, new_refs = (
1321
1475
                    self.interrepo.fetch_refs(update_refs, lossy=False))
1335
1489
        return result
1336
1490
 
1337
1491
    def push(self, overwrite=False, stop_revision=None, lossy=False,
1338
 
             _override_hook_source_branch=None, _stop_revno=None):
 
1492
             _override_hook_source_branch=None, _stop_revno=None,
 
1493
             tag_selector=None):
1339
1494
        result = GitBranchPushResult()
1340
1495
        result.source_branch = self.source
1341
1496
        result.target_branch = self.target
1346
1501
                stop_revision, stop_revno=_stop_revno)
1347
1502
 
1348
1503
            def update_refs(old_refs):
1349
 
                return self._update_refs(result, old_refs, new_refs, overwrite)
 
1504
                return self._update_refs(result, old_refs, new_refs, overwrite, tag_selector)
1350
1505
            try:
1351
1506
                result.revidmap, old_refs, new_refs = (
1352
1507
                    self.interrepo.fetch_refs(
1355
1510
                raise errors.NoRoundtrippingSupport(self.source, self.target)
1356
1511
            (old_sha1, result.old_revid) = old_refs.get(
1357
1512
                main_ref, (ZERO_SHA, NULL_REVISION))
1358
 
            if result.old_revid is None:
 
1513
            if lossy or result.old_revid is None:
1359
1514
                result.old_revid = self.target.lookup_foreign_revision_id(
1360
1515
                    old_sha1)
1361
1516
            result.new_revid = new_refs[main_ref][1]