94 """ |
112 """ |
95 params = json.loads(ui.fin.read()) |
113 params = json.loads(ui.fin.read()) |
96 result = callconduit(repo, name, params) |
114 result = callconduit(repo, name, params) |
97 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': ')) |
115 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': ')) |
98 ui.write('%s\n' % s) |
116 ui.write('%s\n' % s) |
|
117 |
|
118 def getrepophid(repo): |
|
119 """given callsign, return repository PHID or None""" |
|
120 # developer config: phabricator.repophid |
|
121 repophid = repo.ui.config('phabricator', 'repophid') |
|
122 if repophid: |
|
123 return repophid |
|
124 callsign = repo.ui.config('phabricator', 'callsign') |
|
125 if not callsign: |
|
126 return None |
|
127 query = callconduit(repo, 'diffusion.repository.search', |
|
128 {'constraints': {'callsigns': [callsign]}}) |
|
129 if len(query[r'data']) == 0: |
|
130 return None |
|
131 repophid = encoding.strtolocal(query[r'data'][0][r'phid']) |
|
132 repo.ui.setconfig('phabricator', 'repophid', repophid) |
|
133 return repophid |
|
134 |
|
135 _differentialrevisionre = re.compile('\AD([1-9][0-9]*)\Z') |
|
136 |
|
137 def getmapping(ctx): |
|
138 """return (node, associated Differential Revision ID) or (None, None) |
|
139 |
|
140 Examines all precursors and their tags. Tags with format like "D1234" are |
|
141 considered a match and the node with that tag, and the number after "D" |
|
142 (ex. 1234) will be returned. |
|
143 """ |
|
144 unfi = ctx.repo().unfiltered() |
|
145 nodemap = unfi.changelog.nodemap |
|
146 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]): |
|
147 if n in nodemap: |
|
148 for tag in unfi.nodetags(n): |
|
149 m = _differentialrevisionre.match(tag) |
|
150 if m: |
|
151 return n, int(m.group(1)) |
|
152 return None, None |
|
153 |
|
154 def getdiff(ctx, diffopts): |
|
155 """plain-text diff without header (user, commit message, etc)""" |
|
156 output = util.stringio() |
|
157 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(), |
|
158 None, opts=diffopts): |
|
159 output.write(chunk) |
|
160 return output.getvalue() |
|
161 |
|
162 def creatediff(ctx): |
|
163 """create a Differential Diff""" |
|
164 repo = ctx.repo() |
|
165 repophid = getrepophid(repo) |
|
166 # Create a "Differential Diff" via "differential.createrawdiff" API |
|
167 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))} |
|
168 if repophid: |
|
169 params['repositoryPHID'] = repophid |
|
170 diff = callconduit(repo, 'differential.createrawdiff', params) |
|
171 if not diff: |
|
172 raise error.Abort(_('cannot create diff for %s') % ctx) |
|
173 return diff |
|
174 |
|
175 def writediffproperties(ctx, diff): |
|
176 """write metadata to diff so patches could be applied losslessly""" |
|
177 params = { |
|
178 'diff_id': diff[r'id'], |
|
179 'name': 'hg:meta', |
|
180 'data': json.dumps({ |
|
181 'user': ctx.user(), |
|
182 'date': '%d %d' % ctx.date(), |
|
183 }), |
|
184 } |
|
185 callconduit(ctx.repo(), 'differential.setdiffproperty', params) |
|
186 |
|
187 def createdifferentialrevision(ctx, revid=None, parentrevid=None): |
|
188 """create or update a Differential Revision |
|
189 |
|
190 If revid is None, create a new Differential Revision, otherwise update |
|
191 revid. If parentrevid is not None, set it as a dependency. |
|
192 """ |
|
193 repo = ctx.repo() |
|
194 diff = creatediff(ctx) |
|
195 writediffproperties(ctx, diff) |
|
196 |
|
197 transactions = [{'type': 'update', 'value': diff[r'phid']}] |
|
198 |
|
199 # Use a temporary summary to set dependency. There might be better ways but |
|
200 # I cannot find them for now. But do not do that if we are updating an |
|
201 # existing revision (revid is not None) since that introduces visible |
|
202 # churns (someone edited "Summary" twice) on the web page. |
|
203 if parentrevid and revid is None: |
|
204 summary = 'Depends on D%s' % parentrevid |
|
205 transactions += [{'type': 'summary', 'value': summary}, |
|
206 {'type': 'summary', 'value': ' '}] |
|
207 |
|
208 # Parse commit message and update related fields. |
|
209 desc = ctx.description() |
|
210 info = callconduit(repo, 'differential.parsecommitmessage', |
|
211 {'corpus': desc}) |
|
212 for k, v in info[r'fields'].items(): |
|
213 if k in ['title', 'summary', 'testPlan']: |
|
214 transactions.append({'type': k, 'value': v}) |
|
215 |
|
216 params = {'transactions': transactions} |
|
217 if revid is not None: |
|
218 # Update an existing Differential Revision |
|
219 params['objectIdentifier'] = revid |
|
220 |
|
221 revision = callconduit(repo, 'differential.revision.edit', params) |
|
222 if not revision: |
|
223 raise error.Abort(_('cannot create revision for %s') % ctx) |
|
224 |
|
225 return revision |
|
226 |
|
227 @command('phabsend', |
|
228 [('r', 'rev', [], _('revisions to send'), _('REV'))], |
|
229 _('REV [OPTIONS]')) |
|
230 def phabsend(ui, repo, *revs, **opts): |
|
231 """upload changesets to Phabricator |
|
232 |
|
233 If there are multiple revisions specified, they will be send as a stack |
|
234 with a linear dependencies relationship using the order specified by the |
|
235 revset. |
|
236 |
|
237 For the first time uploading changesets, local tags will be created to |
|
238 maintain the association. After the first time, phabsend will check |
|
239 obsstore and tags information so it can figure out whether to update an |
|
240 existing Differential Revision, or create a new one. |
|
241 """ |
|
242 revs = list(revs) + opts.get('rev', []) |
|
243 revs = scmutil.revrange(repo, revs) |
|
244 |
|
245 # Send patches one by one so we know their Differential Revision IDs and |
|
246 # can provide dependency relationship |
|
247 lastrevid = None |
|
248 for rev in revs: |
|
249 ui.debug('sending rev %d\n' % rev) |
|
250 ctx = repo[rev] |
|
251 |
|
252 # Get Differential Revision ID |
|
253 oldnode, revid = getmapping(ctx) |
|
254 if oldnode != ctx.node(): |
|
255 # Create or update Differential Revision |
|
256 revision = createdifferentialrevision(ctx, revid, lastrevid) |
|
257 newrevid = int(revision[r'object'][r'id']) |
|
258 if revid: |
|
259 action = _('updated') |
|
260 else: |
|
261 action = _('created') |
|
262 |
|
263 # Create a local tag to note the association |
|
264 tagname = 'D%d' % newrevid |
|
265 tags.tag(repo, tagname, ctx.node(), message=None, user=None, |
|
266 date=None, local=True) |
|
267 else: |
|
268 # Nothing changed. But still set "newrevid" so the next revision |
|
269 # could depend on this one. |
|
270 newrevid = revid |
|
271 action = _('skipped') |
|
272 |
|
273 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx, |
|
274 ctx.description().split('\n')[0])) |
|
275 lastrevid = newrevid |