5 # This software may be used and distributed according to the terms of the |
5 # This software may be used and distributed according to the terms of the |
6 # GNU General Public License version 2 or any later version. |
6 # GNU General Public License version 2 or any later version. |
7 |
7 |
8 from node import hex, nullid, nullrev, short |
8 from node import hex, nullid, nullrev, short |
9 from i18n import _ |
9 from i18n import _ |
10 import os, sys, errno, re, tempfile |
10 import os, sys, errno, re, tempfile, cStringIO, shutil |
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies |
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies |
12 import match as matchmod |
12 import match as matchmod |
13 import context, repair, graphmod, revset, phases, obsolete, pathutil |
13 import context, repair, graphmod, revset, phases, obsolete, pathutil |
14 import changelog |
14 import changelog |
15 import bookmarks |
15 import bookmarks |
16 import encoding |
16 import encoding |
17 import lock as lockmod |
17 import lock as lockmod |
18 |
18 |
19 def parsealiases(cmd): |
19 def parsealiases(cmd): |
20 return cmd.lstrip("^").split("|") |
20 return cmd.lstrip("^").split("|") |
|
21 |
|
22 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts): |
|
23 import merge as mergemod |
|
24 if not ui.interactive(): |
|
25 raise util.Abort(_('running non-interactively, use %s instead') % |
|
26 cmdsuggest) |
|
27 |
|
28 # make sure username is set before going interactive |
|
29 if not opts.get('user'): |
|
30 ui.username() # raise exception, username not provided |
|
31 |
|
32 def recordfunc(ui, repo, message, match, opts): |
|
33 """This is generic record driver. |
|
34 |
|
35 Its job is to interactively filter local changes, and |
|
36 accordingly prepare working directory into a state in which the |
|
37 job can be delegated to a non-interactive commit command such as |
|
38 'commit' or 'qrefresh'. |
|
39 |
|
40 After the actual job is done by non-interactive command, the |
|
41 working directory is restored to its original state. |
|
42 |
|
43 In the end we'll record interesting changes, and everything else |
|
44 will be left in place, so the user can continue working. |
|
45 """ |
|
46 |
|
47 checkunfinished(repo, commit=True) |
|
48 merge = len(repo[None].parents()) > 1 |
|
49 if merge: |
|
50 raise util.Abort(_('cannot partially commit a merge ' |
|
51 '(use "hg commit" instead)')) |
|
52 |
|
53 status = repo.status(match=match) |
|
54 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True) |
|
55 diffopts.nodates = True |
|
56 diffopts.git = True |
|
57 originalchunks = patch.diff(repo, changes=status, opts=diffopts) |
|
58 fp = cStringIO.StringIO() |
|
59 fp.write(''.join(originalchunks)) |
|
60 fp.seek(0) |
|
61 |
|
62 # 1. filter patch, so we have intending-to apply subset of it |
|
63 try: |
|
64 chunks = patch.filterpatch(ui, patch.parsepatch(fp)) |
|
65 except patch.PatchError, err: |
|
66 raise util.Abort(_('error parsing patch: %s') % err) |
|
67 |
|
68 del fp |
|
69 |
|
70 contenders = set() |
|
71 for h in chunks: |
|
72 try: |
|
73 contenders.update(set(h.files())) |
|
74 except AttributeError: |
|
75 pass |
|
76 |
|
77 changed = status.modified + status.added + status.removed |
|
78 newfiles = [f for f in changed if f in contenders] |
|
79 if not newfiles: |
|
80 ui.status(_('no changes to record\n')) |
|
81 return 0 |
|
82 |
|
83 newandmodifiedfiles = set() |
|
84 for h in chunks: |
|
85 ishunk = isinstance(h, patch.recordhunk) |
|
86 isnew = h.filename() in status.added |
|
87 if ishunk and isnew and not h in originalchunks: |
|
88 newandmodifiedfiles.add(h.filename()) |
|
89 |
|
90 modified = set(status.modified) |
|
91 |
|
92 # 2. backup changed files, so we can restore them in the end |
|
93 |
|
94 if backupall: |
|
95 tobackup = changed |
|
96 else: |
|
97 tobackup = [f for f in newfiles |
|
98 if f in modified or f in newandmodifiedfiles] |
|
99 |
|
100 backups = {} |
|
101 if tobackup: |
|
102 backupdir = repo.join('record-backups') |
|
103 try: |
|
104 os.mkdir(backupdir) |
|
105 except OSError, err: |
|
106 if err.errno != errno.EEXIST: |
|
107 raise |
|
108 try: |
|
109 # backup continues |
|
110 for f in tobackup: |
|
111 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', |
|
112 dir=backupdir) |
|
113 os.close(fd) |
|
114 ui.debug('backup %r as %r\n' % (f, tmpname)) |
|
115 util.copyfile(repo.wjoin(f), tmpname) |
|
116 shutil.copystat(repo.wjoin(f), tmpname) |
|
117 backups[f] = tmpname |
|
118 |
|
119 fp = cStringIO.StringIO() |
|
120 for c in chunks: |
|
121 fname = c.filename() |
|
122 if fname in backups or fname in newandmodifiedfiles: |
|
123 c.write(fp) |
|
124 dopatch = fp.tell() |
|
125 fp.seek(0) |
|
126 |
|
127 [os.unlink(c) for c in newandmodifiedfiles] |
|
128 |
|
129 # 3a. apply filtered patch to clean repo (clean) |
|
130 if backups: |
|
131 # Equivalent to hg.revert |
|
132 choices = lambda key: key in backups |
|
133 mergemod.update(repo, repo.dirstate.p1(), |
|
134 False, True, choices) |
|
135 |
|
136 |
|
137 # 3b. (apply) |
|
138 if dopatch: |
|
139 try: |
|
140 ui.debug('applying patch\n') |
|
141 ui.debug(fp.getvalue()) |
|
142 patch.internalpatch(ui, repo, fp, 1, eolmode=None) |
|
143 except patch.PatchError, err: |
|
144 raise util.Abort(str(err)) |
|
145 del fp |
|
146 |
|
147 # 4. We prepared working directory according to filtered |
|
148 # patch. Now is the time to delegate the job to |
|
149 # commit/qrefresh or the like! |
|
150 |
|
151 # Make all of the pathnames absolute. |
|
152 newfiles = [repo.wjoin(nf) for nf in newfiles] |
|
153 commitfunc(ui, repo, *newfiles, **opts) |
|
154 |
|
155 return 0 |
|
156 finally: |
|
157 # 5. finally restore backed-up files |
|
158 try: |
|
159 for realname, tmpname in backups.iteritems(): |
|
160 ui.debug('restoring %r to %r\n' % (tmpname, realname)) |
|
161 util.copyfile(tmpname, repo.wjoin(realname)) |
|
162 # Our calls to copystat() here and above are a |
|
163 # hack to trick any editors that have f open that |
|
164 # we haven't modified them. |
|
165 # |
|
166 # Also note that this racy as an editor could |
|
167 # notice the file's mtime before we've finished |
|
168 # writing it. |
|
169 shutil.copystat(tmpname, repo.wjoin(realname)) |
|
170 os.unlink(tmpname) |
|
171 if tobackup: |
|
172 os.rmdir(backupdir) |
|
173 except OSError: |
|
174 pass |
|
175 |
|
176 # wrap ui.write so diff output can be labeled/colorized |
|
177 def wrapwrite(orig, *args, **kw): |
|
178 label = kw.pop('label', '') |
|
179 for chunk, l in patch.difflabel(lambda: args): |
|
180 orig(chunk, label=label + l) |
|
181 |
|
182 oldwrite = ui.write |
|
183 def wrap(*args, **kwargs): |
|
184 return wrapwrite(oldwrite, *args, **kwargs) |
|
185 setattr(ui, 'write', wrap) |
|
186 |
|
187 try: |
|
188 return commit(ui, repo, recordfunc, pats, opts) |
|
189 finally: |
|
190 ui.write = oldwrite |
|
191 |
21 |
192 |
22 def findpossible(cmd, table, strict=False): |
193 def findpossible(cmd, table, strict=False): |
23 """ |
194 """ |
24 Return cmd -> (aliases, command table entry) |
195 Return cmd -> (aliases, command table entry) |
25 for each matching command. |
196 for each matching command. |