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