author | Mark J. Nelson <Mark.J.Nelson@Sun.COM> |
Wed, 06 Aug 2008 16:29:39 -0600 | |
changeset 7298 | b69e27387f74 |
parent 7078 | 935563142864 |
child 9006 | c03e0483bda8 |
permissions | -rw-r--r-- |
7078 | 1 |
# |
2 |
# This program is free software; you can redistribute it and/or modify |
|
3 |
# it under the terms of the GNU General Public License version 2 |
|
4 |
# as published by the Free Software Foundation. |
|
5 |
# |
|
6 |
# This program is distributed in the hope that it will be useful, |
|
7 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 |
# GNU General Public License for more details. |
|
10 |
# |
|
11 |
# You should have received a copy of the GNU General Public License |
|
12 |
# along with this program; if not, write to the Free Software |
|
13 |
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
|
14 |
# |
|
15 |
||
16 |
# |
|
17 |
# Copyright 2008 Sun Microsystems, Inc. All rights reserved. |
|
18 |
# Use is subject to license terms. |
|
19 |
# |
|
20 |
||
21 |
# |
|
22 |
# Theory: |
|
23 |
# |
|
24 |
# Workspaces have a non-binding parent/child relationship. |
|
25 |
# All important operations apply to the changes between the two. |
|
26 |
# |
|
27 |
# However, for the sake of remote operation, the 'parent' of a |
|
28 |
# workspace is not seen as a literal entity, instead the figurative |
|
29 |
# parent contains the last changeset common to both parent and child, |
|
30 |
# as such the 'parent tip' is actually nothing of the sort, but instead a |
|
31 |
# convenient imitation. |
|
32 |
# |
|
33 |
# Any change made to a workspace is a change to a file therein, such |
|
34 |
# changes can be represented briefly as whether the file was |
|
35 |
# modified/added/removed as compared to the parent workspace, whether |
|
36 |
# the file has a different name in the parent and if so, whether it |
|
37 |
# was renamed or merely copied. Each changed file has an |
|
38 |
# associated ActiveEntry. |
|
39 |
# |
|
40 |
# The ActiveList being a list ActiveEntrys can thus present the entire |
|
41 |
# change in workspace state between a parent and its child, and is the |
|
42 |
# important bit here (in that if it is incorrect, everything else will |
|
43 |
# be as incorrect, or more) |
|
44 |
# |
|
45 |
||
46 |
import cStringIO |
|
47 |
import os |
|
48 |
from mercurial import hg, patch, cmdutil, util, node, repo |
|
49 |
from mercurial import revlog, repair |
|
50 |
from hgext import mq |
|
51 |
||
52 |
||
53 |
class ActiveEntry(object): |
|
54 |
'''Representation of the changes made to a single file. |
|
55 |
||
56 |
MODIFIED - Contents changed, but no other changes were made |
|
57 |
ADDED - File is newly created |
|
58 |
REMOVED - File is being removed |
|
59 |
||
60 |
Copies are represented by an Entry whose .parentname is non-nil |
|
61 |
||
62 |
Truly copied files have non-nil .parentname and .renamed = False |
|
63 |
Renames have non-nil .parentname and .renamed = True |
|
64 |
||
65 |
Do not access any of this information directly, do so via the |
|
66 |
||
67 |
.is_<change>() methods.''' |
|
68 |
||
69 |
MODIFIED = 1 |
|
70 |
ADDED = 2 |
|
71 |
REMOVED = 3 |
|
72 |
||
73 |
def __init__(self, name): |
|
74 |
self.name = name |
|
75 |
self.change = None |
|
76 |
self.parentname = None |
|
77 |
# As opposed to copied (or neither) |
|
78 |
self.renamed = False |
|
79 |
self.comments = [] |
|
80 |
||
81 |
# |
|
82 |
# ActiveEntrys sort by the name of the file they represent. |
|
83 |
# |
|
84 |
def __cmp__(self, other): |
|
85 |
return cmp(self.name, other.name) |
|
86 |
||
87 |
def is_added(self): |
|
88 |
return self.change == self.ADDED |
|
89 |
||
90 |
def is_modified(self): |
|
91 |
return self.change == self.MODIFIED |
|
92 |
||
93 |
def is_removed(self): |
|
94 |
return self.change == self.REMOVED |
|
95 |
||
96 |
def is_renamed(self): |
|
97 |
return self.parentname and self.renamed |
|
98 |
||
99 |
def is_copied(self): |
|
100 |
return self.parentname and not self.renamed |
|
101 |
||
102 |
||
103 |
class ActiveList(object): |
|
104 |
'''Complete representation of workspace change. |
|
105 |
||
106 |
In practice, a container for ActiveEntrys, and methods to build them, |
|
107 |
update them, and deal with them en masse.''' |
|
108 |
||
109 |
def __init__(self, ws, parenttip, revs=None): |
|
110 |
self._active = {} |
|
111 |
self.ws = ws |
|
112 |
||
113 |
self.revs = revs |
|
114 |
||
115 |
self.base = None |
|
116 |
self.parenttip = parenttip |
|
117 |
||
118 |
# |
|
119 |
# If we couldn't find a parenttip, the two repositories must |
|
120 |
# be unrelated (Hg catches most of this, but this case is valid for it |
|
121 |
# but invalid for us) |
|
122 |
# |
|
123 |
if self.parenttip == None: |
|
124 |
raise util.Abort('repository is unrelated') |
|
125 |
self.localtip = None |
|
126 |
||
127 |
if revs: |
|
128 |
self.base = revs[0] |
|
129 |
self.localtip = revs[-1] |
|
130 |
||
131 |
self._comments = [] |
|
132 |
||
133 |
self._build(revs) |
|
134 |
||
135 |
def _build(self, revs): |
|
136 |
if not revs: |
|
137 |
return |
|
138 |
||
139 |
status = self.ws.status(self.parenttip.node(), self.localtip.node()) |
|
140 |
||
141 |
files = [] |
|
142 |
for ctype in status.values(): |
|
143 |
files.extend(ctype) |
|
144 |
||
145 |
# |
|
146 |
# When a file is renamed, two operations actually occur. |
|
147 |
# A file copy from source to dest and a removal of source. |
|
148 |
# |
|
149 |
# These are represented as two distinct entries in the |
|
150 |
# changectx and status (one on the dest file for the |
|
151 |
# copy, one on the source file for the remove). |
|
152 |
# |
|
153 |
# Since these are unconnected in both the context and |
|
154 |
# status we can only make the association by explicitly |
|
155 |
# looking for it. |
|
156 |
# |
|
157 |
# We deal with this thusly: |
|
158 |
# |
|
159 |
# We maintain a dict dest -> source of all copies |
|
160 |
# (updating dest as appropriate, but leaving source alone). |
|
161 |
# |
|
162 |
# After all other processing, we mark as renamed any pair |
|
163 |
# where source is on the removed list. |
|
164 |
# |
|
165 |
copies = {} |
|
166 |
||
167 |
# |
|
168 |
# Walk revs looking for renames and adding files that |
|
169 |
# are in both change context and status to the active |
|
170 |
# list. |
|
171 |
# |
|
172 |
for ctx in revs: |
|
173 |
desc = ctx.description().splitlines() |
|
174 |
||
175 |
self._comments.extend(desc) |
|
176 |
||
177 |
for fname in ctx.files(): |
|
178 |
# |
|
179 |
# We store comments per-entry as well, for the sake of |
|
180 |
# webrev and similar. We store twice to avoid the problems |
|
181 |
# of uniquifying comments for the general list (and possibly |
|
182 |
# destroying multi-line entities in the process). |
|
183 |
# |
|
184 |
if fname not in self: |
|
185 |
self._addentry(fname) |
|
186 |
self[fname].comments.extend(desc) |
|
187 |
||
188 |
try: |
|
189 |
fctx = ctx.filectx(fname) |
|
190 |
except revlog.LookupError: |
|
191 |
continue |
|
192 |
||
193 |
# |
|
194 |
# NB: .renamed() is a misnomer, this actually checks |
|
195 |
# for copies. |
|
196 |
# |
|
197 |
rn = fctx.renamed() |
|
198 |
if rn: |
|
199 |
# |
|
200 |
# If the source file is a known copy we know its |
|
201 |
# ancestry leads us to the parent. |
|
202 |
# Otherwise make sure the source file is known to |
|
203 |
# be in the parent, we need not care otherwise. |
|
204 |
# |
|
205 |
# We detect cycles at a later point. There is no |
|
206 |
# reason to continuously handle them. |
|
207 |
# |
|
208 |
if rn[0] in copies: |
|
209 |
copies[fname] = copies[rn[0]] |
|
210 |
elif rn[0] in self.parenttip.manifest(): |
|
211 |
copies[fname] = rn[0] |
|
212 |
||
213 |
# |
|
214 |
# Walk the copy list marking as copied any non-cyclic pair |
|
215 |
# where the destination file is still present in the local |
|
216 |
# tip (to avoid ephemeral changes) |
|
217 |
# |
|
218 |
# Where source is removed, mark as renamed, and remove the |
|
219 |
# AL entry for the source file |
|
220 |
# |
|
221 |
for fname, oldname in copies.iteritems(): |
|
222 |
if fname == oldname or fname not in self.localtip.manifest(): |
|
223 |
continue |
|
224 |
||
225 |
self[fname].parentname = oldname |
|
226 |
||
227 |
if oldname in status['removed']: |
|
228 |
self[fname].renamed = True |
|
229 |
if oldname in self: |
|
230 |
del self[oldname] |
|
231 |
||
232 |
# |
|
233 |
# Walk the active list setting the change type for each active |
|
234 |
# file. |
|
235 |
# |
|
236 |
# In the case of modified files that are not renames or |
|
237 |
# copies, we do a content comparison, and drop entries that |
|
238 |
# are not actually modified. |
|
239 |
# |
|
240 |
# We walk a copy of the AL such that we can drop entries |
|
241 |
# within the loop. |
|
242 |
# |
|
243 |
for entry in self._active.values(): |
|
244 |
if entry.name not in files: |
|
245 |
del self[entry.name] |
|
246 |
continue |
|
247 |
||
248 |
if entry.name in status['added']: |
|
249 |
entry.change = ActiveEntry.ADDED |
|
250 |
elif entry.name in status['removed']: |
|
251 |
entry.change = ActiveEntry.REMOVED |
|
252 |
elif entry.name in status['modified']: |
|
253 |
entry.change = ActiveEntry.MODIFIED |
|
254 |
||
255 |
# |
|
256 |
# There are cases during a merge where a file will be in |
|
257 |
# the status return as modified, but in reality be an |
|
258 |
# addition (ie, not in the parenttip). |
|
259 |
# |
|
260 |
# We need to check whether the file is actually present |
|
261 |
# in the parenttip, and set it as an add, if not. |
|
262 |
# |
|
263 |
if entry.name not in self.parenttip.manifest(): |
|
264 |
entry.change = ActiveEntry.ADDED |
|
265 |
elif entry.is_modified() and not entry.parentname: |
|
266 |
if not self.filecmp(entry): |
|
267 |
del self[entry.name] |
|
268 |
continue |
|
269 |
||
270 |
assert entry.change |
|
271 |
||
272 |
def __contains__(self, fname): |
|
273 |
return fname in self._active |
|
274 |
||
275 |
def __getitem__(self, key): |
|
276 |
return self._active[key] |
|
277 |
||
278 |
def __setitem__(self, key, value): |
|
279 |
self._active[key] = value |
|
280 |
||
281 |
def __delitem__(self, key): |
|
282 |
del self._active[key] |
|
283 |
||
284 |
def __iter__(self): |
|
285 |
for entry in self._active.values(): |
|
286 |
yield entry |
|
287 |
||
288 |
def _addentry(self, fname): |
|
289 |
if fname not in self: |
|
290 |
self[fname] = ActiveEntry(fname) |
|
291 |
||
292 |
# |
|
293 |
# Return list of files represented in this AL, |
|
294 |
# including the parent file of a rename |
|
295 |
# |
|
296 |
def files(self): |
|
297 |
ret = self._active.keys() |
|
298 |
||
299 |
ret.extend([x.parentname for x in self |
|
300 |
if x.parentname and x.parentname not in ret]) |
|
301 |
return ret |
|
302 |
||
303 |
def comments(self): |
|
304 |
return self._comments |
|
305 |
||
306 |
# |
|
307 |
# It's not uncommon for a child workspace to itself contain the |
|
308 |
# merge of several other children, with initial branch points in |
|
309 |
# the parent (possibly from the cset a project gate was created |
|
310 |
# from, for instance). |
|
311 |
# |
|
312 |
# Immediately after recommit, this leaves us looking like this: |
|
313 |
# |
|
314 |
# * <- recommitted changeset (real tip) |
|
315 |
# | |
|
316 |
# | * <- Local tip |
|
317 |
# |/| |
|
318 |
# * | <- parent tip |
|
319 |
# | |\ |
|
320 |
# | | | |
|
321 |
# | | |\ |
|
322 |
# | | | | |
|
323 |
# | * | | <- Base |
|
324 |
# |/_/__/ |
|
325 |
# |
|
326 |
# [left-most is parent, next is child, right two being |
|
327 |
# branches in child, intermediate merges parent->child |
|
328 |
# omitted] |
|
329 |
# |
|
330 |
# Obviously stripping base (the first child-specific delta on the |
|
331 |
# main child workspace line) doesn't remove the vestigial branches |
|
332 |
# from other workspaces (or in-workspace branches, or whatever) |
|
333 |
# |
|
334 |
# In reality, what we need to strip in a recommit is any |
|
335 |
# child-specific branch descended from the parent (rather than |
|
336 |
# another part of the child). Note that this by its very nature |
|
337 |
# includes the branch representing the 'main' child workspace. |
|
338 |
# |
|
339 |
# We calculate these by walking from base (which is guaranteed to |
|
340 |
# be the oldest child-local cset) to localtip searching for |
|
341 |
# changesets with only one parent cset, and where that cset is not |
|
342 |
# part of the active list (and is therefore outgoing). |
|
343 |
# |
|
344 |
def bases(self): |
|
345 |
'''Find the bases that in combination define the "old" |
|
346 |
side of a recommitted set of changes, based on AL''' |
|
347 |
||
348 |
get = util.cachefunc(lambda r: self.ws.repo.changectx(r).changeset()) |
|
349 |
||
350 |
# We don't rebuild the AL So the AL local tip is the old tip |
|
351 |
revrange = "%s:%s" % (self.base.rev(), self.localtip.rev()) |
|
352 |
||
353 |
changeiter = cmdutil.walkchangerevs(self.ws.repo.ui, self.ws.repo, |
|
354 |
[], get, {'rev': [revrange]})[0] |
|
355 |
||
356 |
hold = [] |
|
357 |
ret = [] |
|
358 |
alrevs = [x.rev() for x in self.revs] |
|
359 |
for st, rev, fns in changeiter: |
|
360 |
n = self.ws.repo.changelog.node(rev) |
|
361 |
if st == 'add': |
|
362 |
if rev in alrevs: |
|
363 |
hold.append(n) |
|
364 |
elif st == 'iter': |
|
365 |
if n not in hold: |
|
366 |
continue |
|
367 |
||
368 |
p = self.ws.repo.changelog.parents(n) |
|
369 |
if p[1] != node.nullid: |
|
370 |
continue |
|
371 |
||
372 |
if self.ws.repo.changectx(p[0]).rev() not in alrevs: |
|
373 |
ret.append(n) |
|
374 |
return ret |
|
375 |
||
376 |
def tags(self): |
|
377 |
'''Find tags that refer to a changeset in the ActiveList, |
|
378 |
returning a list of 3-tuples (tag, node, is_local) for each. |
|
379 |
||
380 |
We return all instances of a tag that refer to such a node, |
|
381 |
not just that which takes precedence.''' |
|
382 |
||
383 |
if os.path.exists(self.ws.repo.join('localtags')): |
|
384 |
l = self.ws.repo.opener('localtags').readlines() |
|
385 |
ltags = [x.rstrip().split(' ') for x in l] |
|
386 |
else: |
|
387 |
ltags = [] |
|
388 |
||
389 |
# We want to use the tags file from the localtip |
|
390 |
if '.hgtags' in self.localtip.manifest(): |
|
391 |
f = self.localtip.filectx('.hgtags') |
|
392 |
rtags = [x.rstrip().split(' ') for x in f.data().splitlines()] |
|
393 |
else: |
|
394 |
rtags = [] |
|
395 |
||
396 |
nodes = [node.hex(n.node()) for n in self.revs] |
|
397 |
tags = [] |
|
398 |
||
399 |
for nd, name in rtags: |
|
400 |
if nd in nodes: |
|
401 |
tags.append((name, self.ws.repo.lookup(nd), False)) |
|
402 |
||
403 |
for nd, name in ltags: |
|
404 |
if nd in nodes: |
|
405 |
tags.append((name, self.ws.repo.lookup(nd), True)) |
|
406 |
||
407 |
return tags |
|
408 |
||
409 |
def filecmp(self, entry): |
|
410 |
'''Compare two revisions of two files |
|
411 |
||
412 |
Return True if file changed, False otherwise. |
|
413 |
||
414 |
The fast path compares file metadata, slow path is a |
|
415 |
real comparison of file content.''' |
|
416 |
||
417 |
parentfile = self.parenttip.filectx(entry.parentname or entry.name) |
|
418 |
localfile = self.localtip.filectx(entry.name) |
|
419 |
||
420 |
# |
|
421 |
# NB: Keep these ordered such as to make every attempt |
|
422 |
# to short-circuit the more time consuming checks. |
|
423 |
# |
|
424 |
if parentfile.size() != localfile.size(): |
|
425 |
return True |
|
426 |
||
427 |
if parentfile.fileflags() != localfile.fileflags(): |
|
428 |
return True |
|
429 |
||
430 |
if parentfile.cmp(localfile.data()): |
|
431 |
return True |
|
432 |
||
433 |
||
434 |
class WorkSpace(object): |
|
435 |
||
436 |
def __init__(self, repository): |
|
437 |
self.repo = repository |
|
438 |
self.ui = self.repo.ui |
|
439 |
self.name = self.repo.root |
|
440 |
||
441 |
parent = self.repo.ui.expandpath('default') |
|
442 |
if parent == 'default': |
|
443 |
parent = None |
|
444 |
self.parentrepo = parent |
|
445 |
||
446 |
self.activecache = {} |
|
447 |
self.outgoingcache = {} |
|
448 |
||
449 |
def parent(self, spec=None): |
|
450 |
'''Return canonical workspace parent, either SPEC if passed, |
|
451 |
or default parent otherwise''' |
|
452 |
return spec or self.parentrepo |
|
453 |
||
454 |
def _localtip(self, bases, heads): |
|
455 |
'''Return a tuple (changectx, workingctx) representing the most |
|
456 |
representative head to act as the local tip. |
|
457 |
||
458 |
If the working directory is modified, the changectx is its |
|
459 |
tipmost local parent (or tipmost parent, if neither is |
|
460 |
local), and the workingctx is non-null. |
|
461 |
||
462 |
If the working directory is clean, the workingctx is null. |
|
463 |
The changectx is the tip-most local head on the current branch. |
|
464 |
If this can't be determined for some reason (e.g., the parent |
|
465 |
repo is inacessible), changectx is the tip-most head on the |
|
466 |
current branch. |
|
467 |
||
468 |
If the workingctx is non-null it is the actual local tip (and would |
|
469 |
be the local tip in any generated ActiveList, for instance), |
|
470 |
the better parent revision is returned also to aid callers needing |
|
471 |
a real changeset to act as a surrogate for an uncommitted change.''' |
|
472 |
||
473 |
def tipmost_of(nodes): |
|
474 |
return sorted(nodes, cmp=lambda x, y: cmp(x.rev(), y.rev()))[-1] |
|
475 |
||
476 |
# |
|
477 |
# We need a full set of outgoing nodes such that we can limit |
|
478 |
# local branch heads to those which are outgoing |
|
479 |
# |
|
480 |
outnodes = self.repo.changelog.nodesbetween(bases, heads)[0] |
|
481 |
wctx = self.repo.workingctx() |
|
482 |
||
483 |
# |
|
484 |
# A modified working context is seen as a proto-branch, where |
|
485 |
# the 'heads' from our view are the parent revisions of that |
|
486 |
# context. |
|
487 |
# (and the working head is it) |
|
488 |
# |
|
489 |
if (wctx.files() or len(wctx.parents()) > 1 or |
|
490 |
wctx.branch() != wctx.parents()[0].branch()): |
|
491 |
heads = wctx.parents() |
|
492 |
else: |
|
493 |
heads = [self.repo.changectx(n) for n in heads] |
|
494 |
wctx = None |
|
495 |
||
496 |
localchoices = [n for n in heads if n.node() in outnodes] |
|
497 |
return (tipmost_of(localchoices or heads), wctx) |
|
498 |
||
499 |
def _parenttip(self, localtip, parent=None): |
|
500 |
'''Find the closest approximation of the parents tip, as best |
|
501 |
as we can. |
|
502 |
||
503 |
In parent-less workspaces returns our tip (given the best |
|
504 |
we can do is deal with uncommitted changes)''' |
|
505 |
||
506 |
def tipmost_shared(head, outnodes): |
|
507 |
'''Return the tipmost node on the same branch as head that is not |
|
508 |
in outnodes. |
|
509 |
||
510 |
We walk from head to the bottom of the workspace (revision |
|
511 |
0) collecting nodes not in outnodes during the add phase |
|
512 |
and return the first node we see in the iter phase that |
|
513 |
was previously collected. |
|
514 |
||
515 |
See the docstring of mercurial.cmdutil.walkchangerevs() |
|
516 |
for the phased approach to the iterator returned. The |
|
517 |
important part to note is that the 'add' phase gathers |
|
518 |
nodes, which the 'iter' phase then iterates through.''' |
|
519 |
||
520 |
get = util.cachefunc(lambda r: self.repo.changectx(r).changeset()) |
|
521 |
changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [], |
|
522 |
get, {'rev': ['%s:0' % head], |
|
523 |
'follow': True})[0] |
|
524 |
seen = [] |
|
525 |
for st, rev, fns in changeiter: |
|
526 |
n = self.repo.changelog.node(rev) |
|
527 |
if st == 'add': |
|
528 |
if n not in outnodes: |
|
529 |
seen.append(n) |
|
530 |
elif st == 'iter': |
|
531 |
if n in seen: |
|
532 |
return rev |
|
533 |
return None |
|
534 |
||
535 |
tipctx, wctx = localtip |
|
536 |
parent = self.parent(parent) |
|
537 |
outgoing = None |
|
538 |
||
539 |
if parent: |
|
540 |
outgoing = self.findoutgoing(parent) |
|
541 |
||
542 |
if wctx: |
|
543 |
possible_branches = wctx.parents() |
|
544 |
else: |
|
545 |
possible_branches = [tipctx] |
|
546 |
||
547 |
nodes = self.repo.changelog.nodesbetween(outgoing)[0] |
|
548 |
ptips = map(lambda x: tipmost_shared(x.rev(), nodes), possible_branches) |
|
549 |
return self.repo.changectx(sorted(ptips)[-1]) |
|
550 |
||
551 |
def status(self, base=None, head=None): |
|
552 |
'''Translate from the hg 6-tuple status format to a hash keyed |
|
553 |
on change-type''' |
|
554 |
states = ['modified', 'added', 'removed', 'deleted', 'unknown', |
|
555 |
'ignored'] |
|
556 |
chngs = self.repo.status(base, head) |
|
557 |
return dict(zip(states, chngs)) |
|
558 |
||
559 |
# |
|
560 |
# Cache findoutgoing results |
|
561 |
# |
|
562 |
def findoutgoing(self, parent): |
|
563 |
ret = [] |
|
564 |
if parent in self.outgoingcache: |
|
565 |
ret = self.outgoingcache[parent] |
|
566 |
else: |
|
567 |
self.ui.pushbuffer() |
|
568 |
try: |
|
569 |
pws = hg.repository(self.ui, parent) |
|
570 |
ret = self.repo.findoutgoing(pws) |
|
571 |
except repo.RepoError: |
|
572 |
self.ui.warn( |
|
573 |
"Warning: Parent workspace %s is not accessible\n" % parent) |
|
574 |
self.ui.warn("active list will be incomplete\n\n") |
|
575 |
||
576 |
self.outgoingcache[parent] = ret |
|
577 |
self.ui.popbuffer() |
|
578 |
||
579 |
return ret |
|
580 |
||
581 |
def modified(self): |
|
582 |
'''Return a list of files modified in the workspace''' |
|
583 |
wctx = self.repo.workingctx() |
|
584 |
return sorted(wctx.files() + wctx.deleted()) or None |
|
585 |
||
586 |
def merged(self): |
|
587 |
'''Return boolean indicating whether the workspace has an uncommitted |
|
588 |
merge''' |
|
589 |
wctx = self.repo.workingctx() |
|
590 |
return len(wctx.parents()) > 1 |
|
591 |
||
592 |
def branched(self): |
|
593 |
'''Return boolean indicating whether the workspace has an |
|
594 |
uncommitted named branch''' |
|
595 |
||
596 |
wctx = self.repo.workingctx() |
|
597 |
return wctx.branch() != wctx.parents()[0].branch() |
|
598 |
||
599 |
def active(self, parent=None): |
|
600 |
'''Return an ActiveList describing changes between workspace |
|
601 |
and parent workspace (including uncommitted changes). |
|
602 |
If workspace has no parent ActiveList will still describe any |
|
603 |
uncommitted changes''' |
|
604 |
||
605 |
parent = self.parent(parent) |
|
606 |
if parent in self.activecache: |
|
607 |
return self.activecache[parent] |
|
608 |
||
609 |
if parent: |
|
610 |
outgoing = self.findoutgoing(parent) |
|
611 |
else: |
|
612 |
outgoing = [] # No parent, no outgoing nodes |
|
613 |
||
614 |
branchheads = self.repo.heads(start=self.repo.dirstate.parents()[0]) |
|
615 |
ourhead, workinghead = self._localtip(outgoing, branchheads) |
|
616 |
||
617 |
if len(branchheads) > 1: |
|
618 |
self.ui.warn('The current branch has more than one head, ' |
|
619 |
'using %s\n' % ourhead.rev()) |
|
620 |
||
621 |
if workinghead: |
|
622 |
parents = workinghead.parents() |
|
623 |
ctxs = [self.repo.changectx(n) for n in |
|
624 |
self.repo.changelog.nodesbetween(outgoing, |
|
625 |
[h.node() for h in |
|
626 |
parents])[0]] |
|
627 |
ctxs.append(workinghead) |
|
628 |
else: |
|
629 |
ctxs = [self.repo.changectx(n) for n in |
|
630 |
self.repo.changelog.nodesbetween(outgoing, |
|
631 |
[ourhead.node()])[0]] |
|
632 |
||
633 |
act = ActiveList(self, self._parenttip((ourhead, workinghead), parent), |
|
634 |
ctxs) |
|
635 |
||
636 |
self.activecache[parent] = act |
|
637 |
return act |
|
638 |
||
7298
b69e27387f74
6733918 Teamware has retired, please welcome your new manager, Mercurial
Mark J. Nelson <Mark.J.Nelson@Sun.COM>
parents:
7078
diff
changeset
|
639 |
def pdiff(self, pats, opts, parent=None): |
7078 | 640 |
'Return diffs relative to PARENT, as best as we can make out' |
641 |
||
642 |
parent = self.parent(parent) |
|
643 |
act = self.active(parent) |
|
644 |
||
645 |
# |
|
646 |
# act.localtip maybe nil, in the case of uncommitted local |
|
647 |
# changes. |
|
648 |
# |
|
649 |
if not act.revs: |
|
650 |
return |
|
651 |
||
7298
b69e27387f74
6733918 Teamware has retired, please welcome your new manager, Mercurial
Mark J. Nelson <Mark.J.Nelson@Sun.COM>
parents:
7078
diff
changeset
|
652 |
names, match = cmdutil.matchpats(self.repo, pats, opts)[:2] |
b69e27387f74
6733918 Teamware has retired, please welcome your new manager, Mercurial
Mark J. Nelson <Mark.J.Nelson@Sun.COM>
parents:
7078
diff
changeset
|
653 |
opts = patch.diffopts(self.ui, opts) |
b69e27387f74
6733918 Teamware has retired, please welcome your new manager, Mercurial
Mark J. Nelson <Mark.J.Nelson@Sun.COM>
parents:
7078
diff
changeset
|
654 |
|
7078 | 655 |
ret = cStringIO.StringIO() |
656 |
patch.diff(self.repo, act.parenttip.node(), act.localtip.node(), |
|
7298
b69e27387f74
6733918 Teamware has retired, please welcome your new manager, Mercurial
Mark J. Nelson <Mark.J.Nelson@Sun.COM>
parents:
7078
diff
changeset
|
657 |
names, fp=ret, opts=opts, match=match) |
7078 | 658 |
return ret.getvalue() |
659 |
||
660 |
# |
|
661 |
# Theory: |
|
662 |
# |
|
663 |
# We wish to go from a single series of consecutive changesets |
|
664 |
# (possibly including merges with the parent) to a single |
|
665 |
# changeset faithfully representing contents and copy history. |
|
666 |
# |
|
667 |
# We achieve this in a somewhat confusing fashion. |
|
668 |
# |
|
669 |
# - Sanity check the workspace |
|
670 |
# - Update the workspace to tip |
|
671 |
# - Enter into the dirstate the sum total of file contents in the |
|
672 |
# to-be-squished changesets |
|
673 |
# - Commit this in-progress change (which has no changes at all, |
|
674 |
# in reality) On top of the effective parent tip. |
|
675 |
# - Strip the child-local branch(es) (see ActiveList.bases()) |
|
676 |
# |
|
677 |
def squishdeltas(self, active, message, user=None): |
|
678 |
'''Create a single conglomerate changeset, with log message MESSAGE |
|
679 |
containing the changes from ACTIVE. USER, if set, is used |
|
680 |
as the author name. |
|
681 |
||
682 |
The old changes are removed.''' |
|
683 |
||
684 |
def strip_tags(nodes): |
|
685 |
'''Remove any tags referring to the specified nodes.''' |
|
686 |
||
687 |
if os.path.exists(self.repo.join('localtags')): |
|
688 |
fh = self.repo.opener('localtags').readlines() |
|
689 |
tags = [t for t in fh if t.split(' ')[0] not in nodes] |
|
690 |
fh = self.repo.opener('localtags', 'w', atomictemp=True) |
|
691 |
fh.writelines(tags) |
|
692 |
fh.rename() |
|
693 |
||
694 |
if os.path.exists(self.repo.wjoin('.hgtags')): |
|
695 |
fh = self.repo.wopener('.hgtags', 'rb').readlines() |
|
696 |
tags = [t for t in fh if t.split(' ')[0] not in nodes] |
|
697 |
fh = self.repo.wopener('.hgtags', 'wb', atomictemp=True) |
|
698 |
fh.writelines(tags) |
|
699 |
fh.rename() |
|
700 |
||
701 |
wlock = self.repo.wlock() |
|
702 |
lock = self.repo.lock() |
|
703 |
||
704 |
# |
|
705 |
# The files involved need to be present in the workspace and |
|
706 |
# not otherwise molested, rather than the workspace not being |
|
707 |
# modified we also need to prevent files being deleted (but |
|
708 |
# left versioned) too. |
|
709 |
# |
|
710 |
# The easiest way to achieve this is to update the working |
|
711 |
# copy to tip. |
|
712 |
# |
|
713 |
self.clean() |
|
714 |
||
715 |
try: |
|
716 |
strip_tags([node.hex(ctx.node()) for ctx in active.revs]) |
|
717 |
except EnvironmentError, e: |
|
718 |
raise util.Abort('Could not recommit tags: %s\n' % e) |
|
719 |
||
720 |
# |
|
721 |
# For copied files, we need to enter the copy into the |
|
722 |
# dirstate before we force the commit such that the |
|
723 |
# file logs of both branches (old and new) contain |
|
724 |
# representation of the copy. |
|
725 |
# |
|
726 |
parentman = active.parenttip.manifest() |
|
727 |
for entry in active: |
|
728 |
if not entry.is_renamed() and not entry.is_copied(): |
|
729 |
continue |
|
730 |
||
731 |
assert entry.parentname in parentman, \ |
|
732 |
("parentname '%s' (of '%s') not in parent" % |
|
733 |
(entry.parentname, entry.name)) |
|
734 |
||
735 |
# |
|
736 |
# If the source file exists, and used to be versioned |
|
737 |
# this will cause this to become a true copy |
|
738 |
# (re-introducing the source file) |
|
739 |
# |
|
740 |
# We bandaid this, by removing the source file in this |
|
741 |
# case. If we're here, the user has already agreed to this |
|
742 |
# from above. |
|
743 |
# |
|
744 |
if (entry.is_renamed() and |
|
745 |
os.path.exists(self.repo.wjoin(entry.parentname))): |
|
746 |
os.unlink(self.repo.wjoin(entry.parentname)) |
|
747 |
||
748 |
self.repo.copy(entry.parentname, entry.name) |
|
749 |
||
750 |
if active.files(): |
|
751 |
extra = {'branch': active.localtip.branch()} |
|
752 |
self.repo.commit(files=active.files(), text=message, |
|
753 |
user=user, p1=active.parenttip.node(), p2=None, |
|
754 |
extra=extra) |
|
755 |
wsstate = "recommitted changeset" |
|
756 |
self.clean() |
|
757 |
else: |
|
758 |
# |
|
759 |
# If all we're doing is stripping the old nodes, we want to |
|
760 |
# update the working copy such that we're not at a revision |
|
761 |
# that's about to go away. |
|
762 |
# |
|
763 |
wsstate = "tip changeset" |
|
764 |
self.clean(rev=active.parenttip.node()) |
|
765 |
||
766 |
# Silence all the strip and update fun |
|
767 |
self.ui.pushbuffer() |
|
768 |
||
769 |
# |
|
770 |
# We must strip away the old representation of the child |
|
771 |
# branch(es). This may involve stripping a theoretically |
|
772 |
# large number of branches in certain cases |
|
773 |
# |
|
774 |
bases = active.bases() |
|
775 |
try: |
|
776 |
try: |
|
777 |
for basenode in bases: |
|
778 |
repair.strip(self.ui, self.repo, basenode, backup=False) |
|
779 |
except: |
|
780 |
# |
|
781 |
# If this fails, it may leave us in a surprising place in |
|
782 |
# the history. |
|
783 |
# |
|
784 |
# We want to warn the user that something went wrong, |
|
785 |
# and what will happen next, re-raise the exception, and |
|
786 |
# bring the working copy back into a consistent state |
|
787 |
# (which the finally block will do) |
|
788 |
# |
|
789 |
self.ui.warn("stripping failed, your workspace will have " |
|
790 |
"superfluous heads.\n" |
|
791 |
"your workspace has been updated to the " |
|
792 |
"%s.\n" % wsstate) |
|
793 |
raise # Re-raise the exception |
|
794 |
finally: |
|
795 |
# |
|
796 |
# We need to remove Hg's undo information (used for rollback), |
|
797 |
# since it refers to data that will probably not exist after |
|
798 |
# the strip. |
|
799 |
# |
|
800 |
||
801 |
self.clean() |
|
802 |
self.repo.dirstate.write() # Flush the dirstate |
|
803 |
self.repo.invalidate() # Invalidate caches |
|
804 |
||
805 |
if os.path.exists(self.repo.sjoin('undo')): |
|
806 |
try: |
|
807 |
os.unlink(self.repo.sjoin('undo')) |
|
808 |
except EnvironmentError, e: |
|
809 |
raise util.Abort('failed to remove undo data: %s\n' % e) |
|
810 |
||
811 |
self.ui.popbuffer() |
|
812 |
||
813 |
def filepath(self, path): |
|
814 |
'Return the full path to a workspace file.' |
|
815 |
return self.repo.pathto(path) |
|
816 |
||
817 |
def clean(self, rev=None): |
|
818 |
'''Bring workspace up to REV (or tip) forcefully (discarding in |
|
819 |
progress changes)''' |
|
820 |
if rev != None: |
|
821 |
rev = self.repo.lookup(rev) |
|
822 |
else: |
|
823 |
rev = self.repo.changelog.tip() |
|
824 |
||
825 |
wlock = self.repo.wlock() |
|
826 |
hg.clean(self.repo, rev, show_stats=False) |
|
827 |
||
828 |
def mq_applied(self): |
|
829 |
'''True if the workspace has Mq patches applied''' |
|
830 |
q = mq.queue(self.ui, self.repo.join('')) |
|
831 |
return q.applied |