Show More
@@ -0,0 +1,39 b'' | |||||
|
1 | Octopus Merge Support | |||
|
2 | ===================== | |||
|
3 | ||||
|
4 | This will be moderately complicated, as we'll need to synthesize phony | |||
|
5 | changeset entries to explode the octopus into "revisions" that only | |||
|
6 | have two parents each. For today, we can probably just do something like | |||
|
7 | ||||
|
8 | aaaaaaaaaaaaaaaaaaXX{20 bytes of exploded node's hex sha} | |||
|
9 | ||||
|
10 | where XX is a counter (so we could have as many as 255 parents in a | |||
|
11 | git commit - more than I think we'd ever see.) That means that we can | |||
|
12 | install some check in this extension to disallow checking out or | |||
|
13 | otherwise interacting with the `aaaaaaaaaaaaaaaaaa` revisions. | |||
|
14 | ||||
|
15 | ||||
|
16 | Interface Creation | |||
|
17 | ==================== | |||
|
18 | ||||
|
19 | We at least need an interface definition for `changelog` in core that | |||
|
20 | this extension can satisfy, and again for `basicstore`. | |||
|
21 | ||||
|
22 | ||||
|
23 | Reason About Locking | |||
|
24 | ==================== | |||
|
25 | ||||
|
26 | We should spend some time thinking hard about locking, especially on | |||
|
27 | .git/index etc. We're probably adequately locking the _git_ | |||
|
28 | repository, but may not have enough locking correctness in places | |||
|
29 | where hg does locking that git isn't aware of (notably the working | |||
|
30 | copy, which I believe Git does not lock.) | |||
|
31 | ||||
|
32 | Clean up requirements | |||
|
33 | ===================== | |||
|
34 | ||||
|
35 | Right now (for historical reasons, mainly) hgext.git uses a | |||
|
36 | .hg/this-is-git file to detect repositories that should be treated as | |||
|
37 | git. We should look in the .hg/requires for the "git" requirement | |||
|
38 | instead (we already set this requirement, so it's mostly keying off | |||
|
39 | that instead of using an empty file.) |
@@ -0,0 +1,259 b'' | |||||
|
1 | """grant Mercurial the ability to operate on Git repositories. (EXPERIMENTAL) | |||
|
2 | ||||
|
3 | This is currently super experimental. It probably will consume your | |||
|
4 | firstborn a la Rumpelstiltskin, etc. | |||
|
5 | """ | |||
|
6 | ||||
|
7 | from __future__ import absolute_import | |||
|
8 | ||||
|
9 | import os | |||
|
10 | ||||
|
11 | import pygit2 | |||
|
12 | ||||
|
13 | from mercurial.i18n import _ | |||
|
14 | ||||
|
15 | from mercurial import ( | |||
|
16 | commands, | |||
|
17 | error, | |||
|
18 | extensions, | |||
|
19 | localrepo, | |||
|
20 | pycompat, | |||
|
21 | store, | |||
|
22 | util, | |||
|
23 | ) | |||
|
24 | ||||
|
25 | from . import ( | |||
|
26 | dirstate, | |||
|
27 | gitlog, | |||
|
28 | gitutil, | |||
|
29 | index, | |||
|
30 | ) | |||
|
31 | ||||
|
32 | ||||
|
33 | # TODO: extract an interface for this in core | |||
|
34 | class gitstore(object): # store.basicstore): | |||
|
35 | def __init__(self, path, vfstype): | |||
|
36 | self.vfs = vfstype(path) | |||
|
37 | self.path = self.vfs.base | |||
|
38 | self.createmode = store._calcmode(self.vfs) | |||
|
39 | # above lines should go away in favor of: | |||
|
40 | # super(gitstore, self).__init__(path, vfstype) | |||
|
41 | ||||
|
42 | self.git = pygit2.Repository( | |||
|
43 | os.path.normpath(os.path.join(path, b'..', b'.git')) | |||
|
44 | ) | |||
|
45 | self._progress_factory = lambda *args, **kwargs: None | |||
|
46 | ||||
|
47 | @util.propertycache | |||
|
48 | def _db(self): | |||
|
49 | # We lazy-create the database because we want to thread a | |||
|
50 | # progress callback down to the indexing process if it's | |||
|
51 | # required, and we don't have a ui handle in makestore(). | |||
|
52 | return index.get_index(self.git, self._progress_factory) | |||
|
53 | ||||
|
54 | def join(self, f): | |||
|
55 | """Fake store.join method for git repositories. | |||
|
56 | ||||
|
57 | For the most part, store.join is used for @storecache | |||
|
58 | decorators to invalidate caches when various files | |||
|
59 | change. We'll map the ones we care about, and ignore the rest. | |||
|
60 | """ | |||
|
61 | if f in (b'00changelog.i', b'00manifest.i'): | |||
|
62 | # This is close enough: in order for the changelog cache | |||
|
63 | # to be invalidated, HEAD will have to change. | |||
|
64 | return os.path.join(self.path, b'HEAD') | |||
|
65 | elif f == b'lock': | |||
|
66 | # TODO: we probably want to map this to a git lock, I | |||
|
67 | # suspect index.lock. We should figure out what the | |||
|
68 | # most-alike file is in git-land. For now we're risking | |||
|
69 | # bad concurrency errors if another git client is used. | |||
|
70 | return os.path.join(self.path, b'hgit-bogus-lock') | |||
|
71 | elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'): | |||
|
72 | return os.path.join(self.path, b'..', b'.hg', f) | |||
|
73 | raise NotImplementedError(b'Need to pick file for %s.' % f) | |||
|
74 | ||||
|
75 | def changelog(self, trypending): | |||
|
76 | # TODO we don't have a plan for trypending in hg's git support yet | |||
|
77 | return gitlog.changelog(self.git, self._db) | |||
|
78 | ||||
|
79 | def manifestlog(self, repo, storenarrowmatch): | |||
|
80 | # TODO handle storenarrowmatch and figure out if we need the repo arg | |||
|
81 | return gitlog.manifestlog(self.git, self._db) | |||
|
82 | ||||
|
83 | def invalidatecaches(self): | |||
|
84 | pass | |||
|
85 | ||||
|
86 | def write(self, tr=None): | |||
|
87 | # normally this handles things like fncache writes, which we don't have | |||
|
88 | pass | |||
|
89 | ||||
|
90 | ||||
|
91 | def _makestore(orig, requirements, storebasepath, vfstype): | |||
|
92 | if os.path.exists( | |||
|
93 | os.path.join(storebasepath, b'this-is-git') | |||
|
94 | ) and os.path.exists(os.path.join(storebasepath, b'..', b'.git')): | |||
|
95 | return gitstore(storebasepath, vfstype) | |||
|
96 | return orig(requirements, storebasepath, vfstype) | |||
|
97 | ||||
|
98 | ||||
|
99 | class gitfilestorage(object): | |||
|
100 | def file(self, path): | |||
|
101 | if path[0:1] == b'/': | |||
|
102 | path = path[1:] | |||
|
103 | return gitlog.filelog(self.store.git, self.store._db, path) | |||
|
104 | ||||
|
105 | ||||
|
106 | def _makefilestorage(orig, requirements, features, **kwargs): | |||
|
107 | store = kwargs['store'] | |||
|
108 | if isinstance(store, gitstore): | |||
|
109 | return gitfilestorage | |||
|
110 | return orig(requirements, features, **kwargs) | |||
|
111 | ||||
|
112 | ||||
|
113 | def _setupdothg(ui, path): | |||
|
114 | dothg = os.path.join(path, b'.hg') | |||
|
115 | if os.path.exists(dothg): | |||
|
116 | ui.warn(_(b'git repo already initialized for hg\n')) | |||
|
117 | else: | |||
|
118 | os.mkdir(os.path.join(path, b'.hg')) | |||
|
119 | # TODO is it ok to extend .git/info/exclude like this? | |||
|
120 | with open( | |||
|
121 | os.path.join(path, b'.git', b'info', b'exclude'), 'ab' | |||
|
122 | ) as exclude: | |||
|
123 | exclude.write(b'\n.hg\n') | |||
|
124 | with open(os.path.join(dothg, b'this-is-git'), 'wb') as f: | |||
|
125 | pass | |||
|
126 | with open(os.path.join(dothg, b'requirements'), 'wb') as f: | |||
|
127 | f.write(b'git\n') | |||
|
128 | ||||
|
129 | ||||
|
130 | _BMS_PREFIX = 'refs/heads/' | |||
|
131 | ||||
|
132 | ||||
|
133 | class gitbmstore(object): | |||
|
134 | def __init__(self, gitrepo): | |||
|
135 | self.gitrepo = gitrepo | |||
|
136 | ||||
|
137 | def __contains__(self, name): | |||
|
138 | return ( | |||
|
139 | _BMS_PREFIX + pycompat.fsdecode(name) | |||
|
140 | ) in self.gitrepo.references | |||
|
141 | ||||
|
142 | def __iter__(self): | |||
|
143 | for r in self.gitrepo.listall_references(): | |||
|
144 | if r.startswith(_BMS_PREFIX): | |||
|
145 | yield pycompat.fsencode(r[len(_BMS_PREFIX) :]) | |||
|
146 | ||||
|
147 | def __getitem__(self, k): | |||
|
148 | return ( | |||
|
149 | self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)] | |||
|
150 | .peel() | |||
|
151 | .id.raw | |||
|
152 | ) | |||
|
153 | ||||
|
154 | def get(self, k, default=None): | |||
|
155 | try: | |||
|
156 | if k in self: | |||
|
157 | return self[k] | |||
|
158 | return default | |||
|
159 | except pygit2.InvalidSpecError: | |||
|
160 | return default | |||
|
161 | ||||
|
162 | @property | |||
|
163 | def active(self): | |||
|
164 | h = self.gitrepo.references['HEAD'] | |||
|
165 | if not isinstance(h.target, str) or not h.target.startswith( | |||
|
166 | _BMS_PREFIX | |||
|
167 | ): | |||
|
168 | return None | |||
|
169 | return pycompat.fsencode(h.target[len(_BMS_PREFIX) :]) | |||
|
170 | ||||
|
171 | @active.setter | |||
|
172 | def active(self, mark): | |||
|
173 | raise NotImplementedError | |||
|
174 | ||||
|
175 | def names(self, node): | |||
|
176 | r = [] | |||
|
177 | for ref in self.gitrepo.listall_references(): | |||
|
178 | if not ref.startswith(_BMS_PREFIX): | |||
|
179 | continue | |||
|
180 | if self.gitrepo.references[ref].peel().id.raw != node: | |||
|
181 | continue | |||
|
182 | r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :])) | |||
|
183 | return r | |||
|
184 | ||||
|
185 | # Cleanup opportunity: this is *identical* to core's bookmarks store. | |||
|
186 | def expandname(self, bname): | |||
|
187 | if bname == b'.': | |||
|
188 | if self.active: | |||
|
189 | return self.active | |||
|
190 | raise error.RepoLookupError(_(b"no active bookmark")) | |||
|
191 | return bname | |||
|
192 | ||||
|
193 | def applychanges(self, repo, tr, changes): | |||
|
194 | """Apply a list of changes to bookmarks | |||
|
195 | """ | |||
|
196 | # TODO: this should respect transactions, but that's going to | |||
|
197 | # require enlarging the gitbmstore to know how to do in-memory | |||
|
198 | # temporary writes and read those back prior to transaction | |||
|
199 | # finalization. | |||
|
200 | for name, node in changes: | |||
|
201 | if node is None: | |||
|
202 | self.gitrepo.references.delete( | |||
|
203 | _BMS_PREFIX + pycompat.fsdecode(name) | |||
|
204 | ) | |||
|
205 | else: | |||
|
206 | self.gitrepo.references.create( | |||
|
207 | _BMS_PREFIX + pycompat.fsdecode(name), | |||
|
208 | gitutil.togitnode(node), | |||
|
209 | force=True, | |||
|
210 | ) | |||
|
211 | ||||
|
212 | ||||
|
213 | def init(orig, ui, dest=b'.', **opts): | |||
|
214 | if opts.get('git', False): | |||
|
215 | path = os.path.abspath(dest) | |||
|
216 | # TODO: walk up looking for the git repo | |||
|
217 | _setupdothg(ui, path) | |||
|
218 | return 0 | |||
|
219 | return orig(ui, dest=dest, **opts) | |||
|
220 | ||||
|
221 | ||||
|
222 | def reposetup(ui, repo): | |||
|
223 | if isinstance(repo.store, gitstore): | |||
|
224 | orig = repo.__class__ | |||
|
225 | repo.store._progress_factory = repo.ui.makeprogress | |||
|
226 | ||||
|
227 | class gitlocalrepo(orig): | |||
|
228 | def _makedirstate(self): | |||
|
229 | # TODO narrow support here | |||
|
230 | return dirstate.gitdirstate( | |||
|
231 | self.ui, self.vfs.base, self.store.git | |||
|
232 | ) | |||
|
233 | ||||
|
234 | def commit(self, *args, **kwargs): | |||
|
235 | ret = orig.commit(self, *args, **kwargs) | |||
|
236 | tid = self.store.git[gitutil.togitnode(ret)].tree.id | |||
|
237 | # DANGER! This will flush any writes staged to the | |||
|
238 | # index in Git, but we're sidestepping the index in a | |||
|
239 | # way that confuses git when we commit. Alas. | |||
|
240 | self.store.git.index.read_tree(tid) | |||
|
241 | self.store.git.index.write() | |||
|
242 | return ret | |||
|
243 | ||||
|
244 | @property | |||
|
245 | def _bookmarks(self): | |||
|
246 | return gitbmstore(self.store.git) | |||
|
247 | ||||
|
248 | repo.__class__ = gitlocalrepo | |||
|
249 | return repo | |||
|
250 | ||||
|
251 | ||||
|
252 | def extsetup(ui): | |||
|
253 | extensions.wrapfunction(localrepo, b'makestore', _makestore) | |||
|
254 | extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage) | |||
|
255 | # Inject --git flag for `hg init` | |||
|
256 | entry = extensions.wrapcommand(commands.table, b'init', init) | |||
|
257 | entry[1].extend( | |||
|
258 | [(b'', b'git', None, b'setup up a git repository instead of hg')] | |||
|
259 | ) |
@@ -0,0 +1,295 b'' | |||||
|
1 | from __future__ import absolute_import | |||
|
2 | ||||
|
3 | import contextlib | |||
|
4 | import errno | |||
|
5 | import os | |||
|
6 | ||||
|
7 | import pygit2 | |||
|
8 | ||||
|
9 | from mercurial import ( | |||
|
10 | error, | |||
|
11 | extensions, | |||
|
12 | match as matchmod, | |||
|
13 | node as nodemod, | |||
|
14 | pycompat, | |||
|
15 | scmutil, | |||
|
16 | util, | |||
|
17 | ) | |||
|
18 | from mercurial.interfaces import ( | |||
|
19 | dirstate as intdirstate, | |||
|
20 | util as interfaceutil, | |||
|
21 | ) | |||
|
22 | ||||
|
23 | from . import gitutil | |||
|
24 | ||||
|
25 | ||||
|
26 | def readpatternfile(orig, filepath, warn, sourceinfo=False): | |||
|
27 | if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')): | |||
|
28 | return orig(filepath, warn, sourceinfo=False) | |||
|
29 | result = [] | |||
|
30 | warnings = [] | |||
|
31 | with open(filepath, b'rb') as fp: | |||
|
32 | for l in fp: | |||
|
33 | l = l.strip() | |||
|
34 | if not l or l.startswith(b'#'): | |||
|
35 | continue | |||
|
36 | if l.startswith(b'!'): | |||
|
37 | warnings.append(b'unsupported ignore pattern %s' % l) | |||
|
38 | continue | |||
|
39 | if l.startswith(b'/'): | |||
|
40 | result.append(b'rootglob:' + l[1:]) | |||
|
41 | else: | |||
|
42 | result.append(b'relglob:' + l) | |||
|
43 | return result, warnings | |||
|
44 | ||||
|
45 | ||||
|
46 | extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile) | |||
|
47 | ||||
|
48 | ||||
|
49 | _STATUS_MAP = { | |||
|
50 | pygit2.GIT_STATUS_CONFLICTED: b'm', | |||
|
51 | pygit2.GIT_STATUS_CURRENT: b'n', | |||
|
52 | pygit2.GIT_STATUS_IGNORED: b'?', | |||
|
53 | pygit2.GIT_STATUS_INDEX_DELETED: b'r', | |||
|
54 | pygit2.GIT_STATUS_INDEX_MODIFIED: b'n', | |||
|
55 | pygit2.GIT_STATUS_INDEX_NEW: b'a', | |||
|
56 | pygit2.GIT_STATUS_INDEX_RENAMED: b'a', | |||
|
57 | pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n', | |||
|
58 | pygit2.GIT_STATUS_WT_DELETED: b'r', | |||
|
59 | pygit2.GIT_STATUS_WT_MODIFIED: b'n', | |||
|
60 | pygit2.GIT_STATUS_WT_NEW: b'?', | |||
|
61 | pygit2.GIT_STATUS_WT_RENAMED: b'a', | |||
|
62 | pygit2.GIT_STATUS_WT_TYPECHANGE: b'n', | |||
|
63 | pygit2.GIT_STATUS_WT_UNREADABLE: b'?', | |||
|
64 | pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: 'm', | |||
|
65 | } | |||
|
66 | ||||
|
67 | ||||
|
68 | @interfaceutil.implementer(intdirstate.idirstate) | |||
|
69 | class gitdirstate(object): | |||
|
70 | def __init__(self, ui, root, gitrepo): | |||
|
71 | self._ui = ui | |||
|
72 | self._root = os.path.dirname(root) | |||
|
73 | self.git = gitrepo | |||
|
74 | self._plchangecallbacks = {} | |||
|
75 | ||||
|
76 | def p1(self): | |||
|
77 | return self.git.head.peel().id.raw | |||
|
78 | ||||
|
79 | def p2(self): | |||
|
80 | # TODO: MERGE_HEAD? something like that, right? | |||
|
81 | return nodemod.nullid | |||
|
82 | ||||
|
83 | def setparents(self, p1, p2=nodemod.nullid): | |||
|
84 | assert p2 == nodemod.nullid, b'TODO merging support' | |||
|
85 | self.git.head.set_target(gitutil.togitnode(p1)) | |||
|
86 | ||||
|
87 | @util.propertycache | |||
|
88 | def identity(self): | |||
|
89 | return util.filestat.frompath( | |||
|
90 | os.path.join(self._root, b'.git', b'index') | |||
|
91 | ) | |||
|
92 | ||||
|
93 | def branch(self): | |||
|
94 | return b'default' | |||
|
95 | ||||
|
96 | def parents(self): | |||
|
97 | # TODO how on earth do we find p2 if a merge is in flight? | |||
|
98 | return self.p1(), nodemod.nullid | |||
|
99 | ||||
|
100 | def __iter__(self): | |||
|
101 | return (pycompat.fsencode(f.path) for f in self.git.index) | |||
|
102 | ||||
|
103 | def items(self): | |||
|
104 | for ie in self.git.index: | |||
|
105 | yield ie.path, None # value should be a dirstatetuple | |||
|
106 | ||||
|
107 | # py2,3 compat forward | |||
|
108 | iteritems = items | |||
|
109 | ||||
|
110 | def __getitem__(self, filename): | |||
|
111 | try: | |||
|
112 | gs = self.git.status_file(filename) | |||
|
113 | except KeyError: | |||
|
114 | return b'?' | |||
|
115 | return _STATUS_MAP[gs] | |||
|
116 | ||||
|
117 | def __contains__(self, filename): | |||
|
118 | try: | |||
|
119 | gs = self.git.status_file(filename) | |||
|
120 | return _STATUS_MAP[gs] != b'?' | |||
|
121 | except KeyError: | |||
|
122 | return False | |||
|
123 | ||||
|
124 | def status(self, match, subrepos, ignored, clean, unknown): | |||
|
125 | # TODO handling of clean files - can we get that from git.status()? | |||
|
126 | modified, added, removed, deleted, unknown, ignored, clean = ( | |||
|
127 | [], | |||
|
128 | [], | |||
|
129 | [], | |||
|
130 | [], | |||
|
131 | [], | |||
|
132 | [], | |||
|
133 | [], | |||
|
134 | ) | |||
|
135 | gstatus = self.git.status() | |||
|
136 | for path, status in gstatus.items(): | |||
|
137 | path = pycompat.fsencode(path) | |||
|
138 | if status == pygit2.GIT_STATUS_IGNORED: | |||
|
139 | if path.endswith(b'/'): | |||
|
140 | continue | |||
|
141 | ignored.append(path) | |||
|
142 | elif status in ( | |||
|
143 | pygit2.GIT_STATUS_WT_MODIFIED, | |||
|
144 | pygit2.GIT_STATUS_INDEX_MODIFIED, | |||
|
145 | pygit2.GIT_STATUS_WT_MODIFIED | |||
|
146 | | pygit2.GIT_STATUS_INDEX_MODIFIED, | |||
|
147 | ): | |||
|
148 | modified.append(path) | |||
|
149 | elif status == pygit2.GIT_STATUS_INDEX_NEW: | |||
|
150 | added.append(path) | |||
|
151 | elif status == pygit2.GIT_STATUS_WT_NEW: | |||
|
152 | unknown.append(path) | |||
|
153 | elif status == pygit2.GIT_STATUS_WT_DELETED: | |||
|
154 | deleted.append(path) | |||
|
155 | elif status == pygit2.GIT_STATUS_INDEX_DELETED: | |||
|
156 | removed.append(path) | |||
|
157 | else: | |||
|
158 | raise error.Abort( | |||
|
159 | b'unhandled case: status for %r is %r' % (path, status) | |||
|
160 | ) | |||
|
161 | ||||
|
162 | # TODO are we really always sure of status here? | |||
|
163 | return ( | |||
|
164 | False, | |||
|
165 | scmutil.status( | |||
|
166 | modified, added, removed, deleted, unknown, ignored, clean | |||
|
167 | ), | |||
|
168 | ) | |||
|
169 | ||||
|
170 | def flagfunc(self, buildfallback): | |||
|
171 | # TODO we can do better | |||
|
172 | return buildfallback() | |||
|
173 | ||||
|
174 | def getcwd(self): | |||
|
175 | # TODO is this a good way to do this? | |||
|
176 | return os.path.dirname( | |||
|
177 | os.path.dirname(pycompat.fsencode(self.git.path)) | |||
|
178 | ) | |||
|
179 | ||||
|
180 | def normalize(self, path): | |||
|
181 | normed = util.normcase(path) | |||
|
182 | assert normed == path, b"TODO handling of case folding: %s != %s" % ( | |||
|
183 | normed, | |||
|
184 | path, | |||
|
185 | ) | |||
|
186 | return path | |||
|
187 | ||||
|
188 | @property | |||
|
189 | def _checklink(self): | |||
|
190 | return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path))) | |||
|
191 | ||||
|
192 | def copies(self): | |||
|
193 | # TODO support copies? | |||
|
194 | return {} | |||
|
195 | ||||
|
196 | # # TODO what the heck is this | |||
|
197 | _filecache = set() | |||
|
198 | ||||
|
199 | def pendingparentchange(self): | |||
|
200 | # TODO: we need to implement the context manager bits and | |||
|
201 | # correctly stage/revert index edits. | |||
|
202 | return False | |||
|
203 | ||||
|
204 | def write(self, tr): | |||
|
205 | # TODO: call parent change callbacks | |||
|
206 | ||||
|
207 | if tr: | |||
|
208 | ||||
|
209 | def writeinner(category): | |||
|
210 | self.git.index.write() | |||
|
211 | ||||
|
212 | tr.addpending(b'gitdirstate', writeinner) | |||
|
213 | else: | |||
|
214 | self.git.index.write() | |||
|
215 | ||||
|
216 | def pathto(self, f, cwd=None): | |||
|
217 | if cwd is None: | |||
|
218 | cwd = self.getcwd() | |||
|
219 | # TODO core dirstate does something about slashes here | |||
|
220 | assert isinstance(f, bytes) | |||
|
221 | r = util.pathto(self._root, cwd, f) | |||
|
222 | return r | |||
|
223 | ||||
|
224 | def matches(self, match): | |||
|
225 | for x in self.git.index: | |||
|
226 | p = pycompat.fsencode(x.path) | |||
|
227 | if match(p): | |||
|
228 | yield p | |||
|
229 | ||||
|
230 | def normal(self, f, parentfiledata=None): | |||
|
231 | """Mark a file normal and clean.""" | |||
|
232 | # TODO: for now we just let libgit2 re-stat the file. We can | |||
|
233 | # clearly do better. | |||
|
234 | ||||
|
235 | def normallookup(self, f): | |||
|
236 | """Mark a file normal, but possibly dirty.""" | |||
|
237 | # TODO: for now we just let libgit2 re-stat the file. We can | |||
|
238 | # clearly do better. | |||
|
239 | ||||
|
240 | def walk(self, match, subrepos, unknown, ignored, full=True): | |||
|
241 | # TODO: we need to use .status() and not iterate the index, | |||
|
242 | # because the index doesn't force a re-walk and so `hg add` of | |||
|
243 | # a new file without an intervening call to status will | |||
|
244 | # silently do nothing. | |||
|
245 | r = {} | |||
|
246 | cwd = self.getcwd() | |||
|
247 | for path, status in self.git.status().items(): | |||
|
248 | if path.startswith('.hg/'): | |||
|
249 | continue | |||
|
250 | path = pycompat.fsencode(path) | |||
|
251 | if not match(path): | |||
|
252 | continue | |||
|
253 | # TODO construct the stat info from the status object? | |||
|
254 | try: | |||
|
255 | s = os.stat(os.path.join(cwd, path)) | |||
|
256 | except OSError as e: | |||
|
257 | if e.errno != errno.ENOENT: | |||
|
258 | raise | |||
|
259 | continue | |||
|
260 | r[path] = s | |||
|
261 | return r | |||
|
262 | ||||
|
263 | def savebackup(self, tr, backupname): | |||
|
264 | # TODO: figure out a strategy for saving index backups. | |||
|
265 | pass | |||
|
266 | ||||
|
267 | def restorebackup(self, tr, backupname): | |||
|
268 | # TODO: figure out a strategy for saving index backups. | |||
|
269 | pass | |||
|
270 | ||||
|
271 | def add(self, f): | |||
|
272 | self.git.index.add(pycompat.fsdecode(f)) | |||
|
273 | ||||
|
274 | def drop(self, f): | |||
|
275 | self.git.index.remove(pycompat.fsdecode(f)) | |||
|
276 | ||||
|
277 | def remove(self, f): | |||
|
278 | self.git.index.remove(pycompat.fsdecode(f)) | |||
|
279 | ||||
|
280 | def copied(self, path): | |||
|
281 | # TODO: track copies? | |||
|
282 | return None | |||
|
283 | ||||
|
284 | @contextlib.contextmanager | |||
|
285 | def parentchange(self): | |||
|
286 | # TODO: track this maybe? | |||
|
287 | yield | |||
|
288 | ||||
|
289 | def addparentchangecallback(self, category, callback): | |||
|
290 | # TODO: should this be added to the dirstate interface? | |||
|
291 | self._plchangecallbacks[category] = callback | |||
|
292 | ||||
|
293 | def clearbackup(self, tr, backupname): | |||
|
294 | # TODO | |||
|
295 | pass |
@@ -0,0 +1,463 b'' | |||||
|
1 | from __future__ import absolute_import | |||
|
2 | ||||
|
3 | import pygit2 | |||
|
4 | ||||
|
5 | from mercurial.i18n import _ | |||
|
6 | ||||
|
7 | from mercurial import ( | |||
|
8 | ancestor, | |||
|
9 | changelog as hgchangelog, | |||
|
10 | dagop, | |||
|
11 | encoding, | |||
|
12 | error, | |||
|
13 | manifest, | |||
|
14 | node as nodemod, | |||
|
15 | pycompat, | |||
|
16 | ) | |||
|
17 | from mercurial.interfaces import ( | |||
|
18 | repository, | |||
|
19 | util as interfaceutil, | |||
|
20 | ) | |||
|
21 | from mercurial.utils import stringutil | |||
|
22 | from . import ( | |||
|
23 | gitutil, | |||
|
24 | index, | |||
|
25 | manifest as gitmanifest, | |||
|
26 | ) | |||
|
27 | ||||
|
28 | ||||
|
29 | class baselog(object): # revlog.revlog): | |||
|
30 | """Common implementations between changelog and manifestlog.""" | |||
|
31 | ||||
|
32 | def __init__(self, gr, db): | |||
|
33 | self.gitrepo = gr | |||
|
34 | self._db = db | |||
|
35 | ||||
|
36 | def __len__(self): | |||
|
37 | return int( | |||
|
38 | self._db.execute('SELECT COUNT(*) FROM changelog').fetchone()[0] | |||
|
39 | ) | |||
|
40 | ||||
|
41 | def rev(self, n): | |||
|
42 | if n == nodemod.nullid: | |||
|
43 | return -1 | |||
|
44 | t = self._db.execute( | |||
|
45 | 'SELECT rev FROM changelog WHERE node = ?', (gitutil.togitnode(n),) | |||
|
46 | ).fetchone() | |||
|
47 | if t is None: | |||
|
48 | raise error.LookupError(n, b'00changelog.i', _(b'no node %d')) | |||
|
49 | return t[0] | |||
|
50 | ||||
|
51 | def node(self, r): | |||
|
52 | if r == nodemod.nullrev: | |||
|
53 | return nodemod.nullid | |||
|
54 | t = self._db.execute( | |||
|
55 | 'SELECT node FROM changelog WHERE rev = ?', (r,) | |||
|
56 | ).fetchone() | |||
|
57 | if t is None: | |||
|
58 | raise error.LookupError(r, b'00changelog.i', _(b'no node')) | |||
|
59 | return nodemod.bin(t[0]) | |||
|
60 | ||||
|
61 | def hasnode(self, n): | |||
|
62 | t = self._db.execute( | |||
|
63 | 'SELECT node FROM changelog WHERE node = ?', (n,) | |||
|
64 | ).fetchone() | |||
|
65 | return t is not None | |||
|
66 | ||||
|
67 | ||||
|
68 | class baselogindex(object): | |||
|
69 | def __init__(self, log): | |||
|
70 | self._log = log | |||
|
71 | ||||
|
72 | def has_node(self, n): | |||
|
73 | return self._log.rev(n) != -1 | |||
|
74 | ||||
|
75 | def __len__(self): | |||
|
76 | return len(self._log) | |||
|
77 | ||||
|
78 | def __getitem__(self, idx): | |||
|
79 | p1rev, p2rev = self._log.parentrevs(idx) | |||
|
80 | # TODO: it's messy that the index leaks so far out of the | |||
|
81 | # storage layer that we have to implement things like reading | |||
|
82 | # this raw tuple, which exposes revlog internals. | |||
|
83 | return ( | |||
|
84 | # Pretend offset is just the index, since we don't really care. | |||
|
85 | idx, | |||
|
86 | # Same with lengths | |||
|
87 | idx, # length | |||
|
88 | idx, # rawsize | |||
|
89 | -1, # delta base | |||
|
90 | idx, # linkrev TODO is this right? | |||
|
91 | p1rev, | |||
|
92 | p2rev, | |||
|
93 | self._log.node(idx), | |||
|
94 | ) | |||
|
95 | ||||
|
96 | ||||
|
97 | # TODO: an interface for the changelog type? | |||
|
98 | class changelog(baselog): | |||
|
99 | def __contains__(self, rev): | |||
|
100 | try: | |||
|
101 | self.node(rev) | |||
|
102 | return True | |||
|
103 | except error.LookupError: | |||
|
104 | return False | |||
|
105 | ||||
|
106 | @property | |||
|
107 | def filteredrevs(self): | |||
|
108 | # TODO: we should probably add a refs/hg/ namespace for hidden | |||
|
109 | # heads etc, but that's an idea for later. | |||
|
110 | return set() | |||
|
111 | ||||
|
112 | @property | |||
|
113 | def index(self): | |||
|
114 | return baselogindex(self) | |||
|
115 | ||||
|
116 | @property | |||
|
117 | def nodemap(self): | |||
|
118 | r = { | |||
|
119 | nodemod.bin(v[0]): v[1] | |||
|
120 | for v in self._db.execute('SELECT node, rev FROM changelog') | |||
|
121 | } | |||
|
122 | r[nodemod.nullid] = nodemod.nullrev | |||
|
123 | return r | |||
|
124 | ||||
|
125 | def tip(self): | |||
|
126 | t = self._db.execute( | |||
|
127 | 'SELECT node FROM changelog ORDER BY rev DESC LIMIT 1' | |||
|
128 | ).fetchone() | |||
|
129 | if t: | |||
|
130 | return nodemod.bin(t[0]) | |||
|
131 | return nodemod.nullid | |||
|
132 | ||||
|
133 | def revs(self, start=0, stop=None): | |||
|
134 | if stop is None: | |||
|
135 | stop = self.tip() | |||
|
136 | t = self._db.execute( | |||
|
137 | 'SELECT rev FROM changelog ' | |||
|
138 | 'WHERE rev >= ? AND rev <= ? ' | |||
|
139 | 'ORDER BY REV ASC', | |||
|
140 | (start, stop), | |||
|
141 | ) | |||
|
142 | return (int(r[0]) for r in t) | |||
|
143 | ||||
|
144 | def _partialmatch(self, id): | |||
|
145 | if nodemod.wdirhex.startswith(id): | |||
|
146 | raise error.WdirUnsupported | |||
|
147 | candidates = [ | |||
|
148 | nodemod.bin(x[0]) | |||
|
149 | for x in self._db.execute( | |||
|
150 | 'SELECT node FROM changelog WHERE node LIKE ?', (id + b'%',) | |||
|
151 | ) | |||
|
152 | ] | |||
|
153 | if nodemod.nullhex.startswith(id): | |||
|
154 | candidates.append(nodemod.nullid) | |||
|
155 | if len(candidates) > 1: | |||
|
156 | raise error.AmbiguousPrefixLookupError( | |||
|
157 | id, b'00changelog.i', _(b'ambiguous identifier') | |||
|
158 | ) | |||
|
159 | if candidates: | |||
|
160 | return candidates[0] | |||
|
161 | return None | |||
|
162 | ||||
|
163 | def flags(self, rev): | |||
|
164 | return 0 | |||
|
165 | ||||
|
166 | def shortest(self, node, minlength=1): | |||
|
167 | nodehex = nodemod.hex(node) | |||
|
168 | for attempt in pycompat.xrange(minlength, len(nodehex) + 1): | |||
|
169 | candidate = nodehex[:attempt] | |||
|
170 | matches = int( | |||
|
171 | self._db.execute( | |||
|
172 | 'SELECT COUNT(*) FROM changelog WHERE node LIKE ?', | |||
|
173 | (pycompat.sysstr(nodehex + b'%'),), | |||
|
174 | ).fetchone()[0] | |||
|
175 | ) | |||
|
176 | if matches == 1: | |||
|
177 | return candidate | |||
|
178 | return nodehex | |||
|
179 | ||||
|
180 | def headrevs(self, revs=None): | |||
|
181 | realheads = [ | |||
|
182 | int(x[0]) | |||
|
183 | for x in self._db.execute( | |||
|
184 | 'SELECT rev FROM changelog ' | |||
|
185 | 'INNER JOIN heads ON changelog.node = heads.node' | |||
|
186 | ) | |||
|
187 | ] | |||
|
188 | if revs: | |||
|
189 | return sorted([r for r in revs if r in realheads]) | |||
|
190 | return sorted(realheads) | |||
|
191 | ||||
|
192 | def changelogrevision(self, nodeorrev): | |||
|
193 | # Ensure we have a node id | |||
|
194 | if isinstance(nodeorrev, int): | |||
|
195 | n = self.node(nodeorrev) | |||
|
196 | else: | |||
|
197 | n = nodeorrev | |||
|
198 | # handle looking up nullid | |||
|
199 | if n == nodemod.nullid: | |||
|
200 | return hgchangelog._changelogrevision(extra={}) | |||
|
201 | hn = gitutil.togitnode(n) | |||
|
202 | # We've got a real commit! | |||
|
203 | files = [ | |||
|
204 | r[0] | |||
|
205 | for r in self._db.execute( | |||
|
206 | 'SELECT filename FROM changedfiles ' | |||
|
207 | 'WHERE node = ? and filenode != ?', | |||
|
208 | (hn, gitutil.nullgit), | |||
|
209 | ) | |||
|
210 | ] | |||
|
211 | filesremoved = [ | |||
|
212 | r[0] | |||
|
213 | for r in self._db.execute( | |||
|
214 | 'SELECT filename FROM changedfiles ' | |||
|
215 | 'WHERE node = ? and filenode = ?', | |||
|
216 | (hn, nodemod.nullhex), | |||
|
217 | ) | |||
|
218 | ] | |||
|
219 | c = self.gitrepo[hn] | |||
|
220 | return hgchangelog._changelogrevision( | |||
|
221 | manifest=n, # pretend manifest the same as the commit node | |||
|
222 | user=b'%s <%s>' | |||
|
223 | % (c.author.name.encode('utf8'), c.author.email.encode('utf8')), | |||
|
224 | date=(c.author.time, -c.author.offset * 60), | |||
|
225 | files=files, | |||
|
226 | # TODO filesadded in the index | |||
|
227 | filesremoved=filesremoved, | |||
|
228 | description=c.message.encode('utf8'), | |||
|
229 | # TODO do we want to handle extra? how? | |||
|
230 | extra={b'branch': b'default'}, | |||
|
231 | ) | |||
|
232 | ||||
|
233 | def ancestors(self, revs, stoprev=0, inclusive=False): | |||
|
234 | revs = list(revs) | |||
|
235 | tip = self.rev(self.tip()) | |||
|
236 | for r in revs: | |||
|
237 | if r > tip: | |||
|
238 | raise IndexError(b'Invalid rev %r' % r) | |||
|
239 | return ancestor.lazyancestors( | |||
|
240 | self.parentrevs, revs, stoprev=stoprev, inclusive=inclusive | |||
|
241 | ) | |||
|
242 | ||||
|
243 | # Cleanup opportunity: this is *identical* to the revlog.py version | |||
|
244 | def descendants(self, revs): | |||
|
245 | return dagop.descendantrevs(revs, self.revs, self.parentrevs) | |||
|
246 | ||||
|
247 | def reachableroots(self, minroot, heads, roots, includepath=False): | |||
|
248 | return dagop._reachablerootspure( | |||
|
249 | self.parentrevs, minroot, roots, heads, includepath | |||
|
250 | ) | |||
|
251 | ||||
|
252 | # Cleanup opportunity: this is *identical* to the revlog.py version | |||
|
253 | def isancestor(self, a, b): | |||
|
254 | a, b = self.rev(a), self.rev(b) | |||
|
255 | return self.isancestorrev(a, b) | |||
|
256 | ||||
|
257 | # Cleanup opportunity: this is *identical* to the revlog.py version | |||
|
258 | def isancestorrev(self, a, b): | |||
|
259 | if a == nodemod.nullrev: | |||
|
260 | return True | |||
|
261 | elif a == b: | |||
|
262 | return True | |||
|
263 | elif a > b: | |||
|
264 | return False | |||
|
265 | return bool(self.reachableroots(a, [b], [a], includepath=False)) | |||
|
266 | ||||
|
267 | def parentrevs(self, rev): | |||
|
268 | n = self.node(rev) | |||
|
269 | hn = gitutil.togitnode(n) | |||
|
270 | c = self.gitrepo[hn] | |||
|
271 | p1 = p2 = nodemod.nullrev | |||
|
272 | if c.parents: | |||
|
273 | p1 = self.rev(c.parents[0].id.raw) | |||
|
274 | if len(c.parents) > 2: | |||
|
275 | raise error.Abort(b'TODO octopus merge handling') | |||
|
276 | if len(c.parents) == 2: | |||
|
277 | p2 = self.rev(c.parents[0].id.raw) | |||
|
278 | return p1, p2 | |||
|
279 | ||||
|
280 | # Private method is used at least by the tags code. | |||
|
281 | _uncheckedparentrevs = parentrevs | |||
|
282 | ||||
|
283 | def commonancestorsheads(self, a, b): | |||
|
284 | # TODO the revlog verson of this has a C path, so we probably | |||
|
285 | # need to optimize this... | |||
|
286 | a, b = self.rev(a), self.rev(b) | |||
|
287 | return [ | |||
|
288 | self.node(n) | |||
|
289 | for n in ancestor.commonancestorsheads(self.parentrevs, a, b) | |||
|
290 | ] | |||
|
291 | ||||
|
292 | def branchinfo(self, rev): | |||
|
293 | """Git doesn't do named branches, so just put everything on default.""" | |||
|
294 | return b'default', False | |||
|
295 | ||||
|
296 | def delayupdate(self, tr): | |||
|
297 | # TODO: I think we can elide this because we're just dropping | |||
|
298 | # an object in the git repo? | |||
|
299 | pass | |||
|
300 | ||||
|
301 | def add( | |||
|
302 | self, | |||
|
303 | manifest, | |||
|
304 | files, | |||
|
305 | desc, | |||
|
306 | transaction, | |||
|
307 | p1, | |||
|
308 | p2, | |||
|
309 | user, | |||
|
310 | date=None, | |||
|
311 | extra=None, | |||
|
312 | p1copies=None, | |||
|
313 | p2copies=None, | |||
|
314 | filesadded=None, | |||
|
315 | filesremoved=None, | |||
|
316 | ): | |||
|
317 | parents = [] | |||
|
318 | hp1, hp2 = gitutil.togitnode(p1), gitutil.togitnode(p2) | |||
|
319 | if p1 != nodemod.nullid: | |||
|
320 | parents.append(hp1) | |||
|
321 | if p2 and p2 != nodemod.nullid: | |||
|
322 | parents.append(hp2) | |||
|
323 | assert date is not None | |||
|
324 | timestamp, tz = date | |||
|
325 | sig = pygit2.Signature( | |||
|
326 | encoding.unifromlocal(stringutil.person(user)), | |||
|
327 | encoding.unifromlocal(stringutil.email(user)), | |||
|
328 | timestamp, | |||
|
329 | -(tz // 60), | |||
|
330 | ) | |||
|
331 | oid = self.gitrepo.create_commit( | |||
|
332 | None, sig, sig, desc, gitutil.togitnode(manifest), parents | |||
|
333 | ) | |||
|
334 | # Set up an internal reference to force the commit into the | |||
|
335 | # changelog. Hypothetically, we could even use this refs/hg/ | |||
|
336 | # namespace to allow for anonymous heads on git repos, which | |||
|
337 | # would be neat. | |||
|
338 | self.gitrepo.references.create( | |||
|
339 | 'refs/hg/internal/latest-commit', oid, force=True | |||
|
340 | ) | |||
|
341 | # Reindex now to pick up changes. We omit the progress | |||
|
342 | # callback because this will be very quick. | |||
|
343 | index._index_repo(self.gitrepo, self._db) | |||
|
344 | return oid.raw | |||
|
345 | ||||
|
346 | ||||
|
347 | class manifestlog(baselog): | |||
|
348 | def __getitem__(self, node): | |||
|
349 | return self.get(b'', node) | |||
|
350 | ||||
|
351 | def get(self, relpath, node): | |||
|
352 | if node == nodemod.nullid: | |||
|
353 | # TODO: this should almost certainly be a memgittreemanifestctx | |||
|
354 | return manifest.memtreemanifestctx(self, relpath) | |||
|
355 | commit = self.gitrepo[gitutil.togitnode(node)] | |||
|
356 | t = commit.tree | |||
|
357 | if relpath: | |||
|
358 | parts = relpath.split(b'/') | |||
|
359 | for p in parts: | |||
|
360 | te = t[p] | |||
|
361 | t = self.gitrepo[te.id] | |||
|
362 | return gitmanifest.gittreemanifestctx(self.gitrepo, t) | |||
|
363 | ||||
|
364 | ||||
|
365 | @interfaceutil.implementer(repository.ifilestorage) | |||
|
366 | class filelog(baselog): | |||
|
367 | def __init__(self, gr, db, path): | |||
|
368 | super(filelog, self).__init__(gr, db) | |||
|
369 | assert isinstance(path, bytes) | |||
|
370 | self.path = path | |||
|
371 | ||||
|
372 | def read(self, node): | |||
|
373 | if node == nodemod.nullid: | |||
|
374 | return b'' | |||
|
375 | return self.gitrepo[gitutil.togitnode(node)].data | |||
|
376 | ||||
|
377 | def lookup(self, node): | |||
|
378 | if len(node) not in (20, 40): | |||
|
379 | node = int(node) | |||
|
380 | if isinstance(node, int): | |||
|
381 | assert False, b'todo revnums for nodes' | |||
|
382 | if len(node) == 40: | |||
|
383 | node = nodemod.bin(node) | |||
|
384 | hnode = gitutil.togitnode(node) | |||
|
385 | if hnode in self.gitrepo: | |||
|
386 | return node | |||
|
387 | raise error.LookupError(self.path, node, _(b'no match found')) | |||
|
388 | ||||
|
389 | def cmp(self, node, text): | |||
|
390 | """Returns True if text is different than content at `node`.""" | |||
|
391 | return self.read(node) != text | |||
|
392 | ||||
|
393 | def add(self, text, meta, transaction, link, p1=None, p2=None): | |||
|
394 | assert not meta # Should we even try to handle this? | |||
|
395 | return self.gitrepo.create_blob(text).raw | |||
|
396 | ||||
|
397 | def __iter__(self): | |||
|
398 | for clrev in self._db.execute( | |||
|
399 | ''' | |||
|
400 | SELECT rev FROM changelog | |||
|
401 | INNER JOIN changedfiles ON changelog.node = changedfiles.node | |||
|
402 | WHERE changedfiles.filename = ? AND changedfiles.filenode != ? | |||
|
403 | ''', | |||
|
404 | (pycompat.fsdecode(self.path), gitutil.nullgit), | |||
|
405 | ): | |||
|
406 | yield clrev[0] | |||
|
407 | ||||
|
408 | def linkrev(self, fr): | |||
|
409 | return fr | |||
|
410 | ||||
|
411 | def rev(self, node): | |||
|
412 | row = self._db.execute( | |||
|
413 | ''' | |||
|
414 | SELECT rev FROM changelog | |||
|
415 | INNER JOIN changedfiles ON changelog.node = changedfiles.node | |||
|
416 | WHERE changedfiles.filename = ? AND changedfiles.filenode = ?''', | |||
|
417 | (pycompat.fsdecode(self.path), gitutil.togitnode(node)), | |||
|
418 | ).fetchone() | |||
|
419 | if row is None: | |||
|
420 | raise error.LookupError(self.path, node, _(b'no such node')) | |||
|
421 | return int(row[0]) | |||
|
422 | ||||
|
423 | def node(self, rev): | |||
|
424 | maybe = self._db.execute( | |||
|
425 | '''SELECT filenode FROM changedfiles | |||
|
426 | INNER JOIN changelog ON changelog.node = changedfiles.node | |||
|
427 | WHERE changelog.rev = ? AND filename = ? | |||
|
428 | ''', | |||
|
429 | (rev, pycompat.fsdecode(self.path)), | |||
|
430 | ).fetchone() | |||
|
431 | if maybe is None: | |||
|
432 | raise IndexError('gitlog %r out of range %d' % (self.path, rev)) | |||
|
433 | return nodemod.bin(maybe[0]) | |||
|
434 | ||||
|
435 | def parents(self, node): | |||
|
436 | gn = gitutil.togitnode(node) | |||
|
437 | gp = pycompat.fsdecode(self.path) | |||
|
438 | ps = [] | |||
|
439 | for p in self._db.execute( | |||
|
440 | '''SELECT p1filenode, p2filenode FROM changedfiles | |||
|
441 | WHERE filenode = ? AND filename = ? | |||
|
442 | ''', | |||
|
443 | (gn, gp), | |||
|
444 | ).fetchone(): | |||
|
445 | if p is None: | |||
|
446 | commit = self._db.execute( | |||
|
447 | "SELECT node FROM changedfiles " | |||
|
448 | "WHERE filenode = ? AND filename = ?", | |||
|
449 | (gn, gp), | |||
|
450 | ).fetchone()[0] | |||
|
451 | # This filelog is missing some data. Build the | |||
|
452 | # filelog, then recurse (which will always find data). | |||
|
453 | if pycompat.ispy3: | |||
|
454 | commit = commit.decode('ascii') | |||
|
455 | index.fill_in_filelog(self.gitrepo, self._db, commit, gp, gn) | |||
|
456 | return self.parents(node) | |||
|
457 | else: | |||
|
458 | ps.append(nodemod.bin(p)) | |||
|
459 | return ps | |||
|
460 | ||||
|
461 | def renamed(self, node): | |||
|
462 | # TODO: renames/copies | |||
|
463 | return False |
@@ -0,0 +1,26 b'' | |||||
|
1 | """utilities to assist in working with pygit2""" | |||
|
2 | from __future__ import absolute_import | |||
|
3 | ||||
|
4 | from mercurial.node import bin, hex, nullid | |||
|
5 | ||||
|
6 | from mercurial import pycompat | |||
|
7 | ||||
|
8 | ||||
|
9 | def togitnode(n): | |||
|
10 | """Wrapper to convert a Mercurial binary node to a unicode hexlified node. | |||
|
11 | ||||
|
12 | pygit2 and sqlite both need nodes as strings, not bytes. | |||
|
13 | """ | |||
|
14 | assert len(n) == 20 | |||
|
15 | return pycompat.sysstr(hex(n)) | |||
|
16 | ||||
|
17 | ||||
|
18 | def fromgitnode(n): | |||
|
19 | """Opposite of togitnode.""" | |||
|
20 | assert len(n) == 40 | |||
|
21 | if pycompat.ispy3: | |||
|
22 | return bin(n.encode('ascii')) | |||
|
23 | return bin(n) | |||
|
24 | ||||
|
25 | ||||
|
26 | nullgit = togitnode(nullid) |
@@ -0,0 +1,346 b'' | |||||
|
1 | from __future__ import absolute_import | |||
|
2 | ||||
|
3 | import collections | |||
|
4 | import os | |||
|
5 | import sqlite3 | |||
|
6 | ||||
|
7 | import pygit2 | |||
|
8 | ||||
|
9 | from mercurial.i18n import _ | |||
|
10 | ||||
|
11 | from mercurial import ( | |||
|
12 | encoding, | |||
|
13 | error, | |||
|
14 | node as nodemod, | |||
|
15 | pycompat, | |||
|
16 | ) | |||
|
17 | ||||
|
18 | from . import gitutil | |||
|
19 | ||||
|
20 | ||||
|
21 | _CURRENT_SCHEMA_VERSION = 1 | |||
|
22 | _SCHEMA = ( | |||
|
23 | """ | |||
|
24 | CREATE TABLE refs ( | |||
|
25 | -- node and name are unique together. There may be more than one name for | |||
|
26 | -- a given node, and there may be no name at all for a given node (in the | |||
|
27 | -- case of an anonymous hg head). | |||
|
28 | node TEXT NOT NULL, | |||
|
29 | name TEXT | |||
|
30 | ); | |||
|
31 | ||||
|
32 | -- The "possible heads" of the repository, which we use to figure out | |||
|
33 | -- if we need to re-walk the changelog. | |||
|
34 | CREATE TABLE possible_heads ( | |||
|
35 | node TEXT NOT NULL | |||
|
36 | ); | |||
|
37 | ||||
|
38 | -- The topological heads of the changelog, which hg depends on. | |||
|
39 | CREATE TABLE heads ( | |||
|
40 | node TEXT NOT NULL | |||
|
41 | ); | |||
|
42 | ||||
|
43 | -- A total ordering of the changelog | |||
|
44 | CREATE TABLE changelog ( | |||
|
45 | rev INTEGER NOT NULL PRIMARY KEY, | |||
|
46 | node TEXT NOT NULL, | |||
|
47 | p1 TEXT, | |||
|
48 | p2 TEXT | |||
|
49 | ); | |||
|
50 | ||||
|
51 | CREATE UNIQUE INDEX changelog_node_idx ON changelog(node); | |||
|
52 | CREATE UNIQUE INDEX changelog_node_rev_idx ON changelog(rev, node); | |||
|
53 | ||||
|
54 | -- Changed files for each commit, which lets us dynamically build | |||
|
55 | -- filelogs. | |||
|
56 | CREATE TABLE changedfiles ( | |||
|
57 | node TEXT NOT NULL, | |||
|
58 | filename TEXT NOT NULL, | |||
|
59 | -- 40 zeroes for deletions | |||
|
60 | filenode TEXT NOT NULL, | |||
|
61 | -- to handle filelog parentage: | |||
|
62 | p1node TEXT, | |||
|
63 | p1filenode TEXT, | |||
|
64 | p2node TEXT, | |||
|
65 | p2filenode TEXT | |||
|
66 | ); | |||
|
67 | ||||
|
68 | CREATE INDEX changedfiles_nodes_idx | |||
|
69 | ON changedfiles(node); | |||
|
70 | ||||
|
71 | PRAGMA user_version=%d | |||
|
72 | """ | |||
|
73 | % _CURRENT_SCHEMA_VERSION | |||
|
74 | ) | |||
|
75 | ||||
|
76 | ||||
|
77 | def _createdb(path): | |||
|
78 | # print('open db', path) | |||
|
79 | # import traceback | |||
|
80 | # traceback.print_stack() | |||
|
81 | db = sqlite3.connect(encoding.strfromlocal(path)) | |||
|
82 | db.text_factory = bytes | |||
|
83 | ||||
|
84 | res = db.execute('PRAGMA user_version').fetchone()[0] | |||
|
85 | ||||
|
86 | # New database. | |||
|
87 | if res == 0: | |||
|
88 | for statement in _SCHEMA.split(';'): | |||
|
89 | db.execute(statement.strip()) | |||
|
90 | ||||
|
91 | db.commit() | |||
|
92 | ||||
|
93 | elif res == _CURRENT_SCHEMA_VERSION: | |||
|
94 | pass | |||
|
95 | ||||
|
96 | else: | |||
|
97 | raise error.Abort(_(b'sqlite database has unrecognized version')) | |||
|
98 | ||||
|
99 | db.execute('PRAGMA journal_mode=WAL') | |||
|
100 | ||||
|
101 | return db | |||
|
102 | ||||
|
103 | ||||
|
104 | _OUR_ORDER = ( | |||
|
105 | pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_REVERSE | |||
|
106 | ) | |||
|
107 | ||||
|
108 | _DIFF_FLAGS = 1 << 21 # GIT_DIFF_FORCE_BINARY, which isn't exposed by pygit2 | |||
|
109 | ||||
|
110 | ||||
|
111 | def _find_nearest_ancestor_introducing_node( | |||
|
112 | db, gitrepo, file_path, walk_start, filenode | |||
|
113 | ): | |||
|
114 | """Find the nearest ancestor that introduces a file node. | |||
|
115 | ||||
|
116 | Args: | |||
|
117 | db: a handle to our sqlite database. | |||
|
118 | gitrepo: A pygit2.Repository instance. | |||
|
119 | file_path: the path of a file in the repo | |||
|
120 | walk_start: a pygit2.Oid that is a commit where we should start walking | |||
|
121 | for our nearest ancestor. | |||
|
122 | ||||
|
123 | Returns: | |||
|
124 | A hexlified SHA that is the commit ID of the next-nearest parent. | |||
|
125 | """ | |||
|
126 | assert isinstance(file_path, str), 'file_path must be str, got %r' % type( | |||
|
127 | file_path | |||
|
128 | ) | |||
|
129 | assert isinstance(filenode, str), 'filenode must be str, got %r' % type( | |||
|
130 | filenode | |||
|
131 | ) | |||
|
132 | parent_options = { | |||
|
133 | row[0].decode('ascii') | |||
|
134 | for row in db.execute( | |||
|
135 | 'SELECT node FROM changedfiles ' | |||
|
136 | 'WHERE filename = ? AND filenode = ?', | |||
|
137 | (file_path, filenode), | |||
|
138 | ) | |||
|
139 | } | |||
|
140 | inner_walker = gitrepo.walk(walk_start, _OUR_ORDER) | |||
|
141 | for w in inner_walker: | |||
|
142 | if w.id.hex in parent_options: | |||
|
143 | return w.id.hex | |||
|
144 | raise error.ProgrammingError( | |||
|
145 | 'Unable to find introducing commit for %s node %s from %s', | |||
|
146 | (file_path, filenode, walk_start), | |||
|
147 | ) | |||
|
148 | ||||
|
149 | ||||
|
150 | def fill_in_filelog(gitrepo, db, startcommit, path, startfilenode): | |||
|
151 | """Given a starting commit and path, fill in a filelog's parent pointers. | |||
|
152 | ||||
|
153 | Args: | |||
|
154 | gitrepo: a pygit2.Repository | |||
|
155 | db: a handle to our sqlite database | |||
|
156 | startcommit: a hexlified node id for the commit to start at | |||
|
157 | path: the path of the file whose parent pointers we should fill in. | |||
|
158 | filenode: the hexlified node id of the file at startcommit | |||
|
159 | ||||
|
160 | TODO: make filenode optional | |||
|
161 | """ | |||
|
162 | assert isinstance( | |||
|
163 | startcommit, str | |||
|
164 | ), 'startcommit must be str, got %r' % type(startcommit) | |||
|
165 | assert isinstance( | |||
|
166 | startfilenode, str | |||
|
167 | ), 'startfilenode must be str, got %r' % type(startfilenode) | |||
|
168 | visit = collections.deque([(startcommit, startfilenode)]) | |||
|
169 | while visit: | |||
|
170 | cnode, filenode = visit.popleft() | |||
|
171 | commit = gitrepo[cnode] | |||
|
172 | parents = [] | |||
|
173 | for parent in commit.parents: | |||
|
174 | t = parent.tree | |||
|
175 | for comp in path.split('/'): | |||
|
176 | try: | |||
|
177 | t = gitrepo[t[comp].id] | |||
|
178 | except KeyError: | |||
|
179 | break | |||
|
180 | else: | |||
|
181 | introducer = _find_nearest_ancestor_introducing_node( | |||
|
182 | db, gitrepo, path, parent.id, t.id.hex | |||
|
183 | ) | |||
|
184 | parents.append((introducer, t.id.hex)) | |||
|
185 | p1node = p1fnode = p2node = p2fnode = gitutil.nullgit | |||
|
186 | for par, parfnode in parents: | |||
|
187 | found = int( | |||
|
188 | db.execute( | |||
|
189 | 'SELECT COUNT(*) FROM changedfiles WHERE ' | |||
|
190 | 'node = ? AND filename = ? AND filenode = ? AND ' | |||
|
191 | 'p1node NOT NULL', | |||
|
192 | (par, path, parfnode), | |||
|
193 | ).fetchone()[0] | |||
|
194 | ) | |||
|
195 | if found == 0: | |||
|
196 | assert par is not None | |||
|
197 | visit.append((par, parfnode)) | |||
|
198 | if parents: | |||
|
199 | p1node, p1fnode = parents[0] | |||
|
200 | if len(parents) == 2: | |||
|
201 | p2node, p2fnode = parents[1] | |||
|
202 | if len(parents) > 2: | |||
|
203 | raise error.ProgrammingError( | |||
|
204 | b"git support can't handle octopus merges" | |||
|
205 | ) | |||
|
206 | db.execute( | |||
|
207 | 'UPDATE changedfiles SET ' | |||
|
208 | 'p1node = ?, p1filenode = ?, p2node = ?, p2filenode = ? ' | |||
|
209 | 'WHERE node = ? AND filename = ? AND filenode = ?', | |||
|
210 | (p1node, p1fnode, p2node, p2fnode, commit.id.hex, path, filenode), | |||
|
211 | ) | |||
|
212 | db.commit() | |||
|
213 | ||||
|
214 | ||||
|
215 | def _index_repo(gitrepo, db, progress_factory=lambda *args, **kwargs: None): | |||
|
216 | # Identify all references so we can tell the walker to visit all of them. | |||
|
217 | all_refs = gitrepo.listall_references() | |||
|
218 | possible_heads = set() | |||
|
219 | prog = progress_factory(b'refs') | |||
|
220 | for pos, ref in enumerate(all_refs): | |||
|
221 | if prog is not None: | |||
|
222 | prog.update(pos) | |||
|
223 | if not ( | |||
|
224 | ref.startswith('refs/heads/') # local branch | |||
|
225 | or ref.startswith('refs/tags/') # tag | |||
|
226 | or ref.startswith('refs/remotes/') # remote branch | |||
|
227 | or ref.startswith('refs/hg/') # from this extension | |||
|
228 | ): | |||
|
229 | continue | |||
|
230 | try: | |||
|
231 | start = gitrepo.lookup_reference(ref).peel(pygit2.GIT_OBJ_COMMIT) | |||
|
232 | except ValueError: | |||
|
233 | # No commit to be found, so we don't care for hg's purposes. | |||
|
234 | continue | |||
|
235 | possible_heads.add(start.id) | |||
|
236 | # Optimization: if the list of heads hasn't changed, don't | |||
|
237 | # reindex, the changelog. This doesn't matter on small | |||
|
238 | # repositories, but on even moderately deep histories (eg cpython) | |||
|
239 | # this is a very important performance win. | |||
|
240 | # | |||
|
241 | # TODO: we should figure out how to incrementally index history | |||
|
242 | # (preferably by detecting rewinds!) so that we don't have to do a | |||
|
243 | # full changelog walk every time a new commit is created. | |||
|
244 | cache_heads = {x[0] for x in db.execute('SELECT node FROM possible_heads')} | |||
|
245 | walker = None | |||
|
246 | cur_cache_heads = {h.hex for h in possible_heads} | |||
|
247 | if cur_cache_heads == cache_heads: | |||
|
248 | return | |||
|
249 | for start in possible_heads: | |||
|
250 | if walker is None: | |||
|
251 | walker = gitrepo.walk(start, _OUR_ORDER) | |||
|
252 | else: | |||
|
253 | walker.push(start) | |||
|
254 | ||||
|
255 | # Empty out the existing changelog. Even for large-ish histories | |||
|
256 | # we can do the top-level "walk all the commits" dance very | |||
|
257 | # quickly as long as we don't need to figure out the changed files | |||
|
258 | # list. | |||
|
259 | db.execute('DELETE FROM changelog') | |||
|
260 | if prog is not None: | |||
|
261 | prog.complete() | |||
|
262 | prog = progress_factory(b'commits') | |||
|
263 | # This walker is sure to visit all the revisions in history, but | |||
|
264 | # only once. | |||
|
265 | for pos, commit in enumerate(walker): | |||
|
266 | if prog is not None: | |||
|
267 | prog.update(pos) | |||
|
268 | p1 = p2 = nodemod.nullhex | |||
|
269 | if len(commit.parents) > 2: | |||
|
270 | raise error.ProgrammingError( | |||
|
271 | ( | |||
|
272 | b"git support can't handle octopus merges, " | |||
|
273 | b"found a commit with %d parents :(" | |||
|
274 | ) | |||
|
275 | % len(commit.parents) | |||
|
276 | ) | |||
|
277 | if commit.parents: | |||
|
278 | p1 = commit.parents[0].id.hex | |||
|
279 | if len(commit.parents) == 2: | |||
|
280 | p2 = commit.parents[1].id.hex | |||
|
281 | db.execute( | |||
|
282 | 'INSERT INTO changelog (rev, node, p1, p2) VALUES(?, ?, ?, ?)', | |||
|
283 | (pos, commit.id.hex, p1, p2), | |||
|
284 | ) | |||
|
285 | ||||
|
286 | num_changedfiles = db.execute( | |||
|
287 | "SELECT COUNT(*) from changedfiles WHERE node = ?", | |||
|
288 | (commit.id.hex,), | |||
|
289 | ).fetchone()[0] | |||
|
290 | if not num_changedfiles: | |||
|
291 | files = {} | |||
|
292 | # I *think* we only need to check p1 for changed files | |||
|
293 | # (and therefore linkrevs), because any node that would | |||
|
294 | # actually have this commit as a linkrev would be | |||
|
295 | # completely new in this rev. | |||
|
296 | p1 = commit.parents[0].id.hex if commit.parents else None | |||
|
297 | if p1 is not None: | |||
|
298 | patchgen = gitrepo.diff(p1, commit.id.hex, flags=_DIFF_FLAGS) | |||
|
299 | else: | |||
|
300 | patchgen = commit.tree.diff_to_tree( | |||
|
301 | swap=True, flags=_DIFF_FLAGS | |||
|
302 | ) | |||
|
303 | new_files = (p.delta.new_file for p in patchgen) | |||
|
304 | files = { | |||
|
305 | nf.path: nf.id.hex | |||
|
306 | for nf in new_files | |||
|
307 | if nf.id.raw != nodemod.nullid | |||
|
308 | } | |||
|
309 | for p, n in files.items(): | |||
|
310 | # We intentionally set NULLs for any file parentage | |||
|
311 | # information so it'll get demand-computed later. We | |||
|
312 | # used to do it right here, and it was _very_ slow. | |||
|
313 | db.execute( | |||
|
314 | 'INSERT INTO changedfiles (' | |||
|
315 | 'node, filename, filenode, p1node, p1filenode, p2node, ' | |||
|
316 | 'p2filenode) VALUES(?, ?, ?, ?, ?, ?, ?)', | |||
|
317 | (commit.id.hex, p, n, None, None, None, None), | |||
|
318 | ) | |||
|
319 | db.execute('DELETE FROM heads') | |||
|
320 | db.execute('DELETE FROM possible_heads') | |||
|
321 | for hid in possible_heads: | |||
|
322 | h = hid.hex | |||
|
323 | db.execute('INSERT INTO possible_heads (node) VALUES(?)', (h,)) | |||
|
324 | haschild = db.execute( | |||
|
325 | 'SELECT COUNT(*) FROM changelog WHERE p1 = ? OR p2 = ?', (h, h) | |||
|
326 | ).fetchone()[0] | |||
|
327 | if not haschild: | |||
|
328 | db.execute('INSERT INTO heads (node) VALUES(?)', (h,)) | |||
|
329 | ||||
|
330 | db.commit() | |||
|
331 | if prog is not None: | |||
|
332 | prog.complete() | |||
|
333 | ||||
|
334 | ||||
|
335 | def get_index(gitrepo, progress_factory=lambda *args, **kwargs: None): | |||
|
336 | cachepath = os.path.join( | |||
|
337 | pycompat.fsencode(gitrepo.path), b'..', b'.hg', b'cache' | |||
|
338 | ) | |||
|
339 | if not os.path.exists(cachepath): | |||
|
340 | os.makedirs(cachepath) | |||
|
341 | dbpath = os.path.join(cachepath, b'git-commits.sqlite') | |||
|
342 | db = _createdb(dbpath) | |||
|
343 | # TODO check against gitrepo heads before doing a full index | |||
|
344 | # TODO thread a ui.progress call into this layer | |||
|
345 | _index_repo(gitrepo, db, progress_factory) | |||
|
346 | return db |
@@ -0,0 +1,293 b'' | |||||
|
1 | from __future__ import absolute_import | |||
|
2 | ||||
|
3 | import pygit2 | |||
|
4 | ||||
|
5 | from mercurial import ( | |||
|
6 | match as matchmod, | |||
|
7 | pathutil, | |||
|
8 | pycompat, | |||
|
9 | util, | |||
|
10 | ) | |||
|
11 | from mercurial.interfaces import ( | |||
|
12 | repository, | |||
|
13 | util as interfaceutil, | |||
|
14 | ) | |||
|
15 | from . import gitutil | |||
|
16 | ||||
|
17 | ||||
|
18 | @interfaceutil.implementer(repository.imanifestdict) | |||
|
19 | class gittreemanifest(object): | |||
|
20 | """Expose git trees (and optionally a builder's overlay) as a manifestdict. | |||
|
21 | ||||
|
22 | Very similar to mercurial.manifest.treemanifest. | |||
|
23 | """ | |||
|
24 | ||||
|
25 | def __init__(self, git_repo, root_tree, pending_changes): | |||
|
26 | """Initializer. | |||
|
27 | ||||
|
28 | Args: | |||
|
29 | git_repo: The git_repo we're walking (required to look up child | |||
|
30 | trees). | |||
|
31 | root_tree: The root Git tree object for this manifest. | |||
|
32 | pending_changes: A dict in which pending changes will be | |||
|
33 | tracked. The enclosing memgittreemanifestctx will use this to | |||
|
34 | construct any required Tree objects in Git during it's | |||
|
35 | `write()` method. | |||
|
36 | """ | |||
|
37 | self._git_repo = git_repo | |||
|
38 | self._tree = root_tree | |||
|
39 | if pending_changes is None: | |||
|
40 | pending_changes = {} | |||
|
41 | # dict of path: Optional[Tuple(node, flags)] | |||
|
42 | self._pending_changes = pending_changes | |||
|
43 | ||||
|
44 | def _resolve_entry(self, path): | |||
|
45 | """Given a path, load its node and flags, or raise KeyError if missing. | |||
|
46 | ||||
|
47 | This takes into account any pending writes in the builder. | |||
|
48 | """ | |||
|
49 | upath = pycompat.fsdecode(path) | |||
|
50 | ent = None | |||
|
51 | if path in self._pending_changes: | |||
|
52 | val = self._pending_changes[path] | |||
|
53 | if val is None: | |||
|
54 | raise KeyError | |||
|
55 | return val | |||
|
56 | t = self._tree | |||
|
57 | comps = upath.split('/') | |||
|
58 | for comp in comps[:-1]: | |||
|
59 | te = self._tree[comp] | |||
|
60 | t = self._git_repo[te.id] | |||
|
61 | ent = t[comps[-1]] | |||
|
62 | if ent.filemode == pygit2.GIT_FILEMODE_BLOB: | |||
|
63 | flags = b'' | |||
|
64 | elif ent.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE: | |||
|
65 | flags = b'x' | |||
|
66 | elif ent.filemode == pygit2.GIT_FILEMODE_LINK: | |||
|
67 | flags = b'l' | |||
|
68 | else: | |||
|
69 | raise ValueError('unsupported mode %s' % oct(ent.filemode)) | |||
|
70 | return ent.id.raw, flags | |||
|
71 | ||||
|
72 | def __getitem__(self, path): | |||
|
73 | return self._resolve_entry(path)[0] | |||
|
74 | ||||
|
75 | def find(self, path): | |||
|
76 | return self._resolve_entry(path) | |||
|
77 | ||||
|
78 | def __len__(self): | |||
|
79 | return len(list(self.walk(matchmod.always()))) | |||
|
80 | ||||
|
81 | def __nonzero__(self): | |||
|
82 | try: | |||
|
83 | next(iter(self)) | |||
|
84 | return True | |||
|
85 | except StopIteration: | |||
|
86 | return False | |||
|
87 | ||||
|
88 | __bool__ = __nonzero__ | |||
|
89 | ||||
|
90 | def __contains__(self, path): | |||
|
91 | try: | |||
|
92 | self._resolve_entry(path) | |||
|
93 | return True | |||
|
94 | except KeyError: | |||
|
95 | return False | |||
|
96 | ||||
|
97 | def iterkeys(self): | |||
|
98 | return self.walk(matchmod.always()) | |||
|
99 | ||||
|
100 | def keys(self): | |||
|
101 | return list(self.iterkeys()) | |||
|
102 | ||||
|
103 | def __iter__(self): | |||
|
104 | return self.iterkeys() | |||
|
105 | ||||
|
106 | def __setitem__(self, path, node): | |||
|
107 | self._pending_changes[path] = node, self.flags(path) | |||
|
108 | ||||
|
109 | def __delitem__(self, path): | |||
|
110 | # TODO: should probably KeyError for already-deleted files? | |||
|
111 | self._pending_changes[path] = None | |||
|
112 | ||||
|
113 | def filesnotin(self, other, match=None): | |||
|
114 | if match is not None: | |||
|
115 | match = matchmod.badmatch(match, lambda path, msg: None) | |||
|
116 | sm2 = set(other.walk(match)) | |||
|
117 | return {f for f in self.walk(match) if f not in sm2} | |||
|
118 | return {f for f in self if f not in other} | |||
|
119 | ||||
|
120 | @util.propertycache | |||
|
121 | def _dirs(self): | |||
|
122 | return pathutil.dirs(self) | |||
|
123 | ||||
|
124 | def hasdir(self, dir): | |||
|
125 | return dir in self._dirs | |||
|
126 | ||||
|
127 | def diff(self, other, match=None, clean=False): | |||
|
128 | # TODO | |||
|
129 | assert False | |||
|
130 | ||||
|
131 | def setflag(self, path, flag): | |||
|
132 | node, unused_flag = self._resolve_entry(path) | |||
|
133 | self._pending_changes[path] = node, flag | |||
|
134 | ||||
|
135 | def get(self, path, default=None): | |||
|
136 | try: | |||
|
137 | return self._resolve_entry(path)[0] | |||
|
138 | except KeyError: | |||
|
139 | return default | |||
|
140 | ||||
|
141 | def flags(self, path): | |||
|
142 | try: | |||
|
143 | return self._resolve_entry(path)[1] | |||
|
144 | except KeyError: | |||
|
145 | return b'' | |||
|
146 | ||||
|
147 | def copy(self): | |||
|
148 | pass | |||
|
149 | ||||
|
150 | def items(self): | |||
|
151 | for f in self: | |||
|
152 | # TODO: build a proper iterator version of this | |||
|
153 | yield self[f] | |||
|
154 | ||||
|
155 | def iteritems(self): | |||
|
156 | return self.items() | |||
|
157 | ||||
|
158 | def iterentries(self): | |||
|
159 | for f in self: | |||
|
160 | # TODO: build a proper iterator version of this | |||
|
161 | yield self._resolve_entry(f) | |||
|
162 | ||||
|
163 | def text(self): | |||
|
164 | assert False # TODO can this method move out of the manifest iface? | |||
|
165 | ||||
|
166 | def _walkonetree(self, tree, match, subdir): | |||
|
167 | for te in tree: | |||
|
168 | # TODO: can we prune dir walks with the matcher? | |||
|
169 | realname = subdir + pycompat.fsencode(te.name) | |||
|
170 | if te.type == r'tree': | |||
|
171 | for inner in self._walkonetree( | |||
|
172 | self._git_repo[te.id], match, realname + b'/' | |||
|
173 | ): | |||
|
174 | yield inner | |||
|
175 | if not match(realname): | |||
|
176 | continue | |||
|
177 | yield pycompat.fsencode(realname) | |||
|
178 | ||||
|
179 | def walk(self, match): | |||
|
180 | # TODO: this is a very lazy way to merge in the pending | |||
|
181 | # changes. There is absolutely room for optimization here by | |||
|
182 | # being clever about walking over the sets... | |||
|
183 | baseline = set(self._walkonetree(self._tree, match, b'')) | |||
|
184 | deleted = {p for p, v in self._pending_changes.items() if v is None} | |||
|
185 | pend = {p for p in self._pending_changes if match(p)} | |||
|
186 | return iter(sorted((baseline | pend) - deleted)) | |||
|
187 | ||||
|
188 | ||||
|
189 | @interfaceutil.implementer(repository.imanifestrevisionstored) | |||
|
190 | class gittreemanifestctx(object): | |||
|
191 | def __init__(self, repo, gittree): | |||
|
192 | self._repo = repo | |||
|
193 | self._tree = gittree | |||
|
194 | ||||
|
195 | def read(self): | |||
|
196 | return gittreemanifest(self._repo, self._tree, None) | |||
|
197 | ||||
|
198 | def copy(self): | |||
|
199 | # NB: it's important that we return a memgittreemanifestctx | |||
|
200 | # because the caller expects a mutable manifest. | |||
|
201 | return memgittreemanifestctx(self._repo, self._tree) | |||
|
202 | ||||
|
203 | def find(self, path): | |||
|
204 | self.read()[path] | |||
|
205 | ||||
|
206 | ||||
|
207 | @interfaceutil.implementer(repository.imanifestrevisionwritable) | |||
|
208 | class memgittreemanifestctx(object): | |||
|
209 | def __init__(self, repo, tree): | |||
|
210 | self._repo = repo | |||
|
211 | self._tree = tree | |||
|
212 | # dict of path: Optional[Tuple(node, flags)] | |||
|
213 | self._pending_changes = {} | |||
|
214 | ||||
|
215 | def read(self): | |||
|
216 | return gittreemanifest(self._repo, self._tree, self._pending_changes) | |||
|
217 | ||||
|
218 | def copy(self): | |||
|
219 | # TODO: if we have a builder in play, what should happen here? | |||
|
220 | # Maybe we can shuffle copy() into the immutable interface. | |||
|
221 | return memgittreemanifestctx(self._repo, self._tree) | |||
|
222 | ||||
|
223 | def write(self, transaction, link, p1, p2, added, removed, match=None): | |||
|
224 | # We're not (for now, anyway) going to audit filenames, so we | |||
|
225 | # can ignore added and removed. | |||
|
226 | ||||
|
227 | # TODO what does this match argument get used for? hopefully | |||
|
228 | # just narrow? | |||
|
229 | assert not match or isinstance(match, matchmod.alwaysmatcher) | |||
|
230 | ||||
|
231 | touched_dirs = pathutil.dirs(self._pending_changes) | |||
|
232 | trees = { | |||
|
233 | b'': self._tree, | |||
|
234 | } | |||
|
235 | # path: treebuilder | |||
|
236 | builders = { | |||
|
237 | b'': self._repo.TreeBuilder(self._tree), | |||
|
238 | } | |||
|
239 | # get a TreeBuilder for every tree in the touched_dirs set | |||
|
240 | for d in sorted(touched_dirs, key=lambda x: (len(x), x)): | |||
|
241 | if d == b'': | |||
|
242 | # loaded root tree above | |||
|
243 | continue | |||
|
244 | comps = d.split(b'/') | |||
|
245 | full = b'' | |||
|
246 | for part in comps: | |||
|
247 | parent = trees[full] | |||
|
248 | try: | |||
|
249 | new = self._repo[parent[pycompat.fsdecode(part)]] | |||
|
250 | except KeyError: | |||
|
251 | # new directory | |||
|
252 | new = None | |||
|
253 | full += b'/' + part | |||
|
254 | if new is not None: | |||
|
255 | # existing directory | |||
|
256 | trees[full] = new | |||
|
257 | builders[full] = self._repo.TreeBuilder(new) | |||
|
258 | else: | |||
|
259 | # new directory, use an empty dict to easily | |||
|
260 | # generate KeyError as any nested new dirs get | |||
|
261 | # created. | |||
|
262 | trees[full] = {} | |||
|
263 | builders[full] = self._repo.TreeBuilder() | |||
|
264 | for f, info in self._pending_changes.items(): | |||
|
265 | if b'/' not in f: | |||
|
266 | dirname = b'' | |||
|
267 | basename = f | |||
|
268 | else: | |||
|
269 | dirname, basename = f.rsplit(b'/', 1) | |||
|
270 | dirname = b'/' + dirname | |||
|
271 | if info is None: | |||
|
272 | builders[dirname].remove(pycompat.fsdecode(basename)) | |||
|
273 | else: | |||
|
274 | n, fl = info | |||
|
275 | mode = { | |||
|
276 | b'': pygit2.GIT_FILEMODE_BLOB, | |||
|
277 | b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE, | |||
|
278 | b'l': pygit2.GIT_FILEMODE_LINK, | |||
|
279 | }[fl] | |||
|
280 | builders[dirname].insert( | |||
|
281 | pycompat.fsdecode(basename), gitutil.togitnode(n), mode | |||
|
282 | ) | |||
|
283 | # This visits the buffered TreeBuilders in deepest-first | |||
|
284 | # order, bubbling up the edits. | |||
|
285 | for b in sorted(builders, key=len, reverse=True): | |||
|
286 | if b == b'': | |||
|
287 | break | |||
|
288 | cb = builders[b] | |||
|
289 | dn, bn = b.rsplit(b'/', 1) | |||
|
290 | builders[dn].insert( | |||
|
291 | pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE | |||
|
292 | ) | |||
|
293 | return builders[b''].write().raw |
@@ -0,0 +1,223 b'' | |||||
|
1 | This test requires pygit2: | |||
|
2 | > $PYTHON -c 'import pygit2' || exit 80 | |||
|
3 | ||||
|
4 | Setup: | |||
|
5 | > GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME | |||
|
6 | > GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL | |||
|
7 | > GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE | |||
|
8 | > GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME | |||
|
9 | > GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL | |||
|
10 | > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE | |||
|
11 | ||||
|
12 | > count=10 | |||
|
13 | > gitcommit() { | |||
|
14 | > GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000"; | |||
|
15 | > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" | |||
|
16 | > git commit "$@" >/dev/null 2>/dev/null || echo "git commit error" | |||
|
17 | > count=`expr $count + 1` | |||
|
18 | > } | |||
|
19 | ||||
|
20 | > echo "[extensions]" >> $HGRCPATH | |||
|
21 | > echo "git=" >> $HGRCPATH | |||
|
22 | ||||
|
23 | Make a new repo with git: | |||
|
24 | $ mkdir foo | |||
|
25 | $ cd foo | |||
|
26 | $ git init | |||
|
27 | Initialized empty Git repository in $TESTTMP/foo/.git/ | |||
|
28 | Ignore the .hg directory within git: | |||
|
29 | $ echo .hg >> .git/info/exclude | |||
|
30 | $ echo alpha > alpha | |||
|
31 | $ git add alpha | |||
|
32 | $ gitcommit -am 'Add alpha' | |||
|
33 | $ echo beta > beta | |||
|
34 | $ git add beta | |||
|
35 | $ gitcommit -am 'Add beta' | |||
|
36 | $ echo gamma > gamma | |||
|
37 | $ git status | |||
|
38 | On branch master | |||
|
39 | Untracked files: | |||
|
40 | (use "git add <file>..." to include in what will be committed) | |||
|
41 | gamma | |||
|
42 | ||||
|
43 | nothing added to commit but untracked files present (use "git add" to track) | |||
|
44 | ||||
|
45 | Without creating the .hg, hg status fails: | |||
|
46 | $ hg status | |||
|
47 | abort: no repository found in '$TESTTMP/foo' (.hg not found)! | |||
|
48 | [255] | |||
|
49 | But if you run hg init --git, it works: | |||
|
50 | $ hg init --git | |||
|
51 | $ hg id --traceback | |||
|
52 | 3d9be8deba43 tip master | |||
|
53 | $ hg status | |||
|
54 | ? gamma | |||
|
55 | Log works too: | |||
|
56 | $ hg log | |||
|
57 | changeset: 1:3d9be8deba43 | |||
|
58 | bookmark: master | |||
|
59 | tag: tip | |||
|
60 | user: test <test@example.org> | |||
|
61 | date: Mon Jan 01 00:00:11 2007 +0000 | |||
|
62 | summary: Add beta | |||
|
63 | ||||
|
64 | changeset: 0:c5864c9d16fb | |||
|
65 | user: test <test@example.org> | |||
|
66 | date: Mon Jan 01 00:00:10 2007 +0000 | |||
|
67 | summary: Add alpha | |||
|
68 | ||||
|
69 | ||||
|
70 | ||||
|
71 | and bookmarks: | |||
|
72 | $ hg bookmarks | |||
|
73 | * master 1:3d9be8deba43 | |||
|
74 | ||||
|
75 | diff even works transparently in both systems: | |||
|
76 | $ echo blah >> alpha | |||
|
77 | $ git diff | |||
|
78 | diff --git a/alpha b/alpha | |||
|
79 | index 4a58007..faed1b7 100644 | |||
|
80 | --- a/alpha | |||
|
81 | +++ b/alpha | |||
|
82 | @@ -1* +1,2 @@ (glob) | |||
|
83 | alpha | |||
|
84 | +blah | |||
|
85 | $ hg diff --git | |||
|
86 | diff --git a/alpha b/alpha | |||
|
87 | --- a/alpha | |||
|
88 | +++ b/alpha | |||
|
89 | @@ -1,1 +1,2 @@ | |||
|
90 | alpha | |||
|
91 | +blah | |||
|
92 | ||||
|
93 | Remove a file, it shows as such: | |||
|
94 | $ rm alpha | |||
|
95 | $ hg status | |||
|
96 | ! alpha | |||
|
97 | ? gamma | |||
|
98 | ||||
|
99 | Revert works: | |||
|
100 | $ hg revert alpha --traceback | |||
|
101 | $ hg status | |||
|
102 | ? gamma | |||
|
103 | $ git status | |||
|
104 | On branch master | |||
|
105 | Untracked files: | |||
|
106 | (use "git add <file>..." to include in what will be committed) | |||
|
107 | gamma | |||
|
108 | ||||
|
109 | nothing added to commit but untracked files present (use "git add" to track) | |||
|
110 | ||||
|
111 | Add shows sanely in both: | |||
|
112 | $ hg add gamma | |||
|
113 | $ hg status | |||
|
114 | A gamma | |||
|
115 | $ hg files | |||
|
116 | alpha | |||
|
117 | beta | |||
|
118 | gamma | |||
|
119 | $ git ls-files | |||
|
120 | alpha | |||
|
121 | beta | |||
|
122 | gamma | |||
|
123 | $ git status | |||
|
124 | On branch master | |||
|
125 | Changes to be committed: | |||
|
126 | (use "git restore --staged <file>..." to unstage) | |||
|
127 | new file: gamma | |||
|
128 | ||||
|
129 | ||||
|
130 | forget does what it should as well: | |||
|
131 | $ hg forget gamma | |||
|
132 | $ hg status | |||
|
133 | ? gamma | |||
|
134 | $ git status | |||
|
135 | On branch master | |||
|
136 | Untracked files: | |||
|
137 | (use "git add <file>..." to include in what will be committed) | |||
|
138 | gamma | |||
|
139 | ||||
|
140 | nothing added to commit but untracked files present (use "git add" to track) | |||
|
141 | ||||
|
142 | clean up untracked file | |||
|
143 | $ rm gamma | |||
|
144 | ||||
|
145 | hg log FILE | |||
|
146 | ||||
|
147 | $ echo a >> alpha | |||
|
148 | $ hg ci -m 'more alpha' --traceback --date '1583522787 18000' | |||
|
149 | $ echo b >> beta | |||
|
150 | $ hg ci -m 'more beta' | |||
|
151 | $ echo a >> alpha | |||
|
152 | $ hg ci -m 'even more alpha' | |||
|
153 | $ hg log -G alpha | |||
|
154 | @ changeset: 4:6626247b7dc8 | |||
|
155 | : bookmark: master | |||
|
156 | : tag: tip | |||
|
157 | : user: test <test> | |||
|
158 | : date: Thu Jan 01 00:00:00 1970 +0000 | |||
|
159 | : summary: even more alpha | |||
|
160 | : | |||
|
161 | o changeset: 2:a1983dd7fb19 | |||
|
162 | : user: test <test> | |||
|
163 | : date: Fri Mar 06 14:26:27 2020 -0500 | |||
|
164 | : summary: more alpha | |||
|
165 | : | |||
|
166 | o changeset: 0:c5864c9d16fb | |||
|
167 | user: test <test@example.org> | |||
|
168 | date: Mon Jan 01 00:00:10 2007 +0000 | |||
|
169 | summary: Add alpha | |||
|
170 | ||||
|
171 | $ hg log -G beta | |||
|
172 | o changeset: 3:d8ee22687733 | |||
|
173 | : user: test <test> | |||
|
174 | : date: Thu Jan 01 00:00:00 1970 +0000 | |||
|
175 | : summary: more beta | |||
|
176 | : | |||
|
177 | o changeset: 1:3d9be8deba43 | |||
|
178 | | user: test <test@example.org> | |||
|
179 | ~ date: Mon Jan 01 00:00:11 2007 +0000 | |||
|
180 | summary: Add beta | |||
|
181 | ||||
|
182 | ||||
|
183 | node|shortest works correctly | |||
|
184 | $ hg log -r tip --template "{node|shortest}\n" | |||
|
185 | 6626 | |||
|
186 | ||||
|
187 | hg annotate | |||
|
188 | ||||
|
189 | $ hg annotate alpha | |||
|
190 | 0: alpha | |||
|
191 | 2: a | |||
|
192 | 4: a | |||
|
193 | $ hg annotate beta | |||
|
194 | 1: beta | |||
|
195 | 3: b | |||
|
196 | ||||
|
197 | ||||
|
198 | Files in subdirectories. TODO: case-folding support, make this `A` | |||
|
199 | instead of `a`. | |||
|
200 | ||||
|
201 | $ mkdir a | |||
|
202 | $ echo "This is file mu." > a/mu | |||
|
203 | $ hg ci -A -m 'Introduce file a/mu' | |||
|
204 | adding a/mu | |||
|
205 | ||||
|
206 | Both hg and git agree a/mu is part of the repo | |||
|
207 | ||||
|
208 | $ git ls-files | |||
|
209 | a/mu | |||
|
210 | alpha | |||
|
211 | beta | |||
|
212 | $ hg files | |||
|
213 | a/mu | |||
|
214 | alpha | |||
|
215 | beta | |||
|
216 | ||||
|
217 | hg and git status both clean | |||
|
218 | ||||
|
219 | $ git status | |||
|
220 | On branch master | |||
|
221 | nothing to commit, working tree clean | |||
|
222 | $ hg status | |||
|
223 |
@@ -1214,6 +1214,7 b' packages = [' | |||||
1214 | 'hgext.fsmonitor', |
|
1214 | 'hgext.fsmonitor', | |
1215 | 'hgext.fastannotate', |
|
1215 | 'hgext.fastannotate', | |
1216 | 'hgext.fsmonitor.pywatchman', |
|
1216 | 'hgext.fsmonitor.pywatchman', | |
|
1217 | 'hgext.git', | |||
1217 | 'hgext.highlight', |
|
1218 | 'hgext.highlight', | |
1218 | 'hgext.hooklib', |
|
1219 | 'hgext.hooklib', | |
1219 | 'hgext.infinitepush', |
|
1220 | 'hgext.infinitepush', |
General Comments 0
You need to be logged in to leave comments.
Login now