1# logexchange.py
2#
3# Copyright 2017 Augie Fackler <raf@durin42.com>
4# Copyright 2017 Sean Farley <sean@farley.io>
5#
6# This software may be used and distributed according to the terms of the
7# GNU General Public License version 2 or any later version.
8
9from __future__ import absolute_import
10
11from .node import hex
12
13from . import (
14    pycompat,
15    util,
16    vfs as vfsmod,
17)
18from .utils import (
19    urlutil,
20)
21
22# directory name in .hg/ in which remotenames files will be present
23remotenamedir = b'logexchange'
24
25
26def readremotenamefile(repo, filename):
27    """
28    reads a file from .hg/logexchange/ directory and yields it's content
29    filename: the file to be read
30    yield a tuple (node, remotepath, name)
31    """
32
33    vfs = vfsmod.vfs(repo.vfs.join(remotenamedir))
34    if not vfs.exists(filename):
35        return
36    f = vfs(filename)
37    lineno = 0
38    for line in f:
39        line = line.strip()
40        if not line:
41            continue
42        # contains the version number
43        if lineno == 0:
44            lineno += 1
45        try:
46            node, remote, rname = line.split(b'\0')
47            yield node, remote, rname
48        except ValueError:
49            pass
50
51    f.close()
52
53
54def readremotenames(repo):
55    """
56    read the details about the remotenames stored in .hg/logexchange/ and
57    yields a tuple (node, remotepath, name). It does not yields information
58    about whether an entry yielded is branch or bookmark. To get that
59    information, call the respective functions.
60    """
61
62    for bmentry in readremotenamefile(repo, b'bookmarks'):
63        yield bmentry
64    for branchentry in readremotenamefile(repo, b'branches'):
65        yield branchentry
66
67
68def writeremotenamefile(repo, remotepath, names, nametype):
69    vfs = vfsmod.vfs(repo.vfs.join(remotenamedir))
70    f = vfs(nametype, b'w', atomictemp=True)
71    # write the storage version info on top of file
72    # version '0' represents the very initial version of the storage format
73    f.write(b'0\n\n')
74
75    olddata = set(readremotenamefile(repo, nametype))
76    # re-save the data from a different remote than this one.
77    for node, oldpath, rname in sorted(olddata):
78        if oldpath != remotepath:
79            f.write(b'%s\0%s\0%s\n' % (node, oldpath, rname))
80
81    for name, node in sorted(pycompat.iteritems(names)):
82        if nametype == b"branches":
83            for n in node:
84                f.write(b'%s\0%s\0%s\n' % (n, remotepath, name))
85        elif nametype == b"bookmarks":
86            if node:
87                f.write(b'%s\0%s\0%s\n' % (node, remotepath, name))
88
89    f.close()
90
91
92def saveremotenames(repo, remotepath, branches=None, bookmarks=None):
93    """
94    save remotenames i.e. remotebookmarks and remotebranches in their
95    respective files under ".hg/logexchange/" directory.
96    """
97    wlock = repo.wlock()
98    try:
99        if bookmarks:
100            writeremotenamefile(repo, remotepath, bookmarks, b'bookmarks')
101        if branches:
102            writeremotenamefile(repo, remotepath, branches, b'branches')
103    finally:
104        wlock.release()
105
106
107def activepath(repo, remote):
108    """returns remote path"""
109    # is the remote a local peer
110    local = remote.local()
111
112    # determine the remote path from the repo, if possible; else just
113    # use the string given to us
114    rpath = remote
115    if local:
116        rpath = util.pconvert(remote._repo.root)
117    elif not isinstance(remote, bytes):
118        rpath = remote._url
119
120    # represent the remotepath with user defined path name if exists
121    for path, url in repo.ui.configitems(b'paths'):
122        # remove auth info from user defined url
123        noauthurl = urlutil.removeauth(url)
124
125        # Standardize on unix style paths, otherwise some {remotenames} end up
126        # being an absolute path on Windows.
127        url = util.pconvert(bytes(url))
128        noauthurl = util.pconvert(noauthurl)
129        if url == rpath or noauthurl == rpath:
130            rpath = path
131            break
132
133    return rpath
134
135
136def pullremotenames(localrepo, remoterepo):
137    """
138    pulls bookmarks and branches information of the remote repo during a
139    pull or clone operation.
140    localrepo is our local repository
141    remoterepo is the peer instance
142    """
143    remotepath = activepath(localrepo, remoterepo)
144
145    with remoterepo.commandexecutor() as e:
146        bookmarks = e.callcommand(
147            b'listkeys',
148            {
149                b'namespace': b'bookmarks',
150            },
151        ).result()
152
153    # on a push, we don't want to keep obsolete heads since
154    # they won't show up as heads on the next pull, so we
155    # remove them here otherwise we would require the user
156    # to issue a pull to refresh the storage
157    bmap = {}
158    repo = localrepo.unfiltered()
159
160    with remoterepo.commandexecutor() as e:
161        branchmap = e.callcommand(b'branchmap', {}).result()
162
163    for branch, nodes in pycompat.iteritems(branchmap):
164        bmap[branch] = []
165        for node in nodes:
166            if node in repo and not repo[node].obsolete():
167                bmap[branch].append(hex(node))
168
169    saveremotenames(localrepo, remotepath, bmap, bookmarks)
170