Mercurial > evolve
comparison hgext/qsync.py @ 153:c088f503cc97
add qsync extension to mutable history
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Tue, 20 Mar 2012 16:11:57 +0100 |
parents | |
children | ccbadfae1d06 |
comparison
equal
deleted
inserted
replaced
152:54c67d7f9eed | 153:c088f503cc97 |
---|---|
1 | |
2 import re | |
3 | |
4 from cStringIO import StringIO | |
5 | |
6 from mercurial.i18n import _ | |
7 from mercurial import commands | |
8 from mercurial import patch | |
9 from mercurial import util | |
10 from mercurial.node import nullid, hex, short, bin | |
11 from mercurial import cmdutil | |
12 from mercurial import hg | |
13 from mercurial import scmutil | |
14 from mercurial import error | |
15 from mercurial import extensions | |
16 | |
17 | |
18 import re | |
19 | |
20 import json | |
21 | |
22 | |
23 ### old compat code | |
24 ############################# | |
25 | |
26 BRANCHNAME="qsubmit2" | |
27 OLDBRANCHNAME="pyves-qsubmit" | |
28 | |
29 ### new command | |
30 ############################# | |
31 cmdtable = {} | |
32 command = cmdutil.command(cmdtable) | |
33 | |
34 @command('^qsync|sync', | |
35 [ | |
36 ('a', 'review-all', False, _('mark all touched patches ready for review (no editor)')), | |
37 ], | |
38 '') | |
39 def cmdsync(ui, repo, **opts): | |
40 '''Export draft changeset as mq patch in a mq patches repository commit. | |
41 | |
42 This command get all changesets in draft phase and create an mq changeset: | |
43 | |
44 * on a "qsubmit2" branch (based on the last changeset) | |
45 | |
46 * one patch per draft changeset | |
47 | |
48 * a series files listing all generated patch | |
49 | |
50 * qsubmitdata holding useful information | |
51 | |
52 It does use obsolete relation to update patches that already existing in the qsubmit2 branch. | |
53 | |
54 Already existing patch which became public, draft or got killed are remove from the mq repo. | |
55 | |
56 Patch name are generated using the summary line for changeset description. | |
57 | |
58 .. warning:: Series files is ordered topologically. So two series with | |
59 interleaved changeset will appear interleaved. | |
60 ''' | |
61 | |
62 review = None | |
63 review = 'edit' | |
64 if opts['review_all']: | |
65 review = 'all' | |
66 mqrepo = repo.mq.qrepo() | |
67 try: | |
68 parent = mqrepo[BRANCHNAME] | |
69 except error.RepoLookupError: | |
70 try: | |
71 parent = mqrepo[OLDBRANCHNAME] | |
72 except error.RepoLookupError: | |
73 parent = initqsubmit(mqrepo) | |
74 store, data, touched = fillstore(repo, parent) | |
75 if not touched: | |
76 raise util.Abort('Nothing changed') | |
77 files = ['qsubmitdata', 'series'] + touched | |
78 # mark some as ready for review | |
79 message = 'qsubmit commit\n\n' | |
80 review_list = [] | |
81 if review: | |
82 for patch_name in touched: | |
83 try: | |
84 store.getfile(patch_name) | |
85 review_list.append(patch_name) | |
86 except IOError: | |
87 pass | |
88 | |
89 if review: | |
90 message += '\n'.join('* %s ready for review' % x for x in review_list) | |
91 memctx = patch.makememctx(mqrepo, (parent.node(), nullid), | |
92 message, | |
93 None, | |
94 None, | |
95 parent.branch(), files, store, | |
96 editor=None) | |
97 if review == 'edit': | |
98 memctx._text = cmdutil.commitforceeditor(mqrepo, memctx, []) | |
99 mqrepo.savecommitmessage(memctx.description()) | |
100 n = memctx.commit() | |
101 return 0 | |
102 | |
103 | |
104 def makename(ctx): | |
105 """create a patch name form a changeset""" | |
106 descsummary = ctx.description().splitlines()[0] | |
107 descsummary = re.sub(r'\s+', '_', descsummary) | |
108 descsummary = re.sub(r'\W+', '', descsummary) | |
109 if len(descsummary) > 45: | |
110 descsummary = descsummary[:42] + '.' | |
111 return '%s-%s.diff' % (ctx.branch().upper(), descsummary) | |
112 | |
113 | |
114 def get_old_data(mqctx): | |
115 """read qsubmit data to fetch previous export data | |
116 | |
117 get old data from the content of an mq commit""" | |
118 try: | |
119 old_data = mqctx['qsubmitdata'] | |
120 return json.loads(old_data.data()) | |
121 except error.LookupError: | |
122 return [] | |
123 | |
124 def get_current_data(repo): | |
125 """Return what would be exported if not previous data exists""" | |
126 data = [] | |
127 for ctx in repo.set('draft() - obsolete()'): | |
128 name = makename(ctx) | |
129 data.append([ctx.hex(), makename(ctx)]) | |
130 return data | |
131 | |
132 | |
133 def patchmq(repo, store, olddata, newdata): | |
134 """export the mq patches and return all useful data to be exported""" | |
135 finaldata = [] | |
136 touched = set() | |
137 currentdrafts = set(d[0] for d in newdata) | |
138 usednew = set() | |
139 usedold = set() | |
140 obsolete = extensions.find('obsolete') | |
141 for oldhex, oldname in olddata: | |
142 if oldhex in usedold: | |
143 continue # no duplicate | |
144 usedold.add(oldhex) | |
145 oldname = str(oldname) | |
146 oldnode = bin(oldhex) | |
147 newnodes = obsolete.newerversion(repo, oldnode) | |
148 if newnodes: | |
149 newnodes = [n for n in newnodes if n] # remove killing | |
150 if len(newnodes) > 1: | |
151 raise util.Abort('%s have more than one newer version: %s'% (oldname, newnodes)) | |
152 if newnodes: | |
153 # else, changeset have been killed | |
154 newnode = list(newnodes)[0][0] | |
155 ctx = repo[newnode] | |
156 if ctx.hex() != oldhex and ctx.phase(): | |
157 fp = StringIO() | |
158 cmdutil.export(repo, [ctx.rev()], fp=fp) | |
159 data = fp.getvalue() | |
160 store.setfile(oldname, data, (None, None)) | |
161 finaldata.append([ctx.hex(), oldname]) | |
162 usednew.add(ctx.hex()) | |
163 touched.add(oldname) | |
164 continue | |
165 if oldhex in currentdrafts: | |
166 # else changeset is now public or secret | |
167 finaldata.append([oldhex, oldname]) | |
168 usednew.add(ctx.hex()) | |
169 continue | |
170 touched.add(oldname) | |
171 | |
172 for newhex, newname in newdata: | |
173 if newhex in usednew: | |
174 continue | |
175 newnode = bin(newhex) | |
176 ctx = repo[newnode] | |
177 fp = StringIO() | |
178 cmdutil.export(repo, [ctx.rev()], fp=fp) | |
179 data = fp.getvalue() | |
180 store.setfile(newname, data, (None, None)) | |
181 finaldata.append([ctx.hex(), newname]) | |
182 touched.add(newname) | |
183 # sort by branchrev number | |
184 finaldata.sort(key=lambda x: sort_key(repo[x[0]])) | |
185 # sort touched too (ease review list) | |
186 stouched = [f[1] for f in finaldata if f[1] in touched] | |
187 return finaldata, stouched | |
188 | |
189 def sort_key(ctx): | |
190 """ctx sort key: (branch, rev)""" | |
191 return (ctx.branch(), ctx.rev()) | |
192 | |
193 | |
194 def fillstore(repo, basemqctx): | |
195 """file store with patch data""" | |
196 olddata = get_old_data(basemqctx) | |
197 newdata = get_current_data(repo) | |
198 store = patch.filestore() | |
199 try: | |
200 data, touched = patchmq(repo, store, olddata, newdata) | |
201 # put all name in the series | |
202 series ='\n'.join(d[1] for d in data) + '\n' | |
203 store.setfile('series', series, (False, False)) | |
204 | |
205 # export data to ease futur work | |
206 series ='\n'.join(d[1] for d in data) + '\n' | |
207 store.setfile('qsubmitdata', json.dumps(data, indent=True), | |
208 (False, False)) | |
209 finally: | |
210 store.close() | |
211 return store, data, touched | |
212 | |
213 | |
214 def initqsubmit(mqrepo): | |
215 """create initial qsubmit branch""" | |
216 store = patch.filestore() | |
217 try: | |
218 files = set() | |
219 store.setfile('.hgignore', '^status$\n', (False, False)) | |
220 memctx = patch.makememctx(mqrepo, (nullid, nullid), | |
221 'qsubmit init', | |
222 None, | |
223 None, | |
224 BRANCHNAME, ('.hgignore',), store, | |
225 editor=None) | |
226 mqrepo.savecommitmessage(memctx.description()) | |
227 n = memctx.commit() | |
228 finally: | |
229 store.close() | |
230 return mqrepo[n] |