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]