Source

hgattic / attic.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#!/usr/bin/env python

# attic.py - noninteractive shelves for mercurial
#
# Copyright 2009 Bill Barry <after.fallout@gmail.com>
#
# Portions liberally adapted from mq.py
# Copyright 2005, 2006 Chris Mason <mason@suse.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

"""patch management and development

This extension lets you work with a set of disjoint patches in a 
Mercurial repository. With it you can task switch between many patches
in a single repository and easily share patch sets between 
repositories.

Known patches are represented as patch files in the .hg/attic
directory.  Applied patches are changes in the working copy.

Common tasks (use "hg help command" for more details):

attic-shelve (shelve):
    store the current working copy changes in a patch in the attic and 
    prepare to work on something else

attic-display (attic, ls):
    list the patches in the attic
    
attic-finish (sfinish):
    change a patch into a changeset (use commit, user and date info
    from patch)
    
attic-current (sactive):
    display the current patch being worked on
     
attic-unshelve (unshelve):
    activate a patch to work on
    
attic-rebuild (rebuild):
    update a patch with the current changes
"""

from mercurial.i18n import _
from mercurial.node import bin, hex, short
from mercurial.repo import RepoError
from mercurial import commands, cmdutil, hg, patch, revlog, util
from mercurial import extensions, repair
import os, sys, re, errno

normname = util.normpath

class attic:
    """encapsulates all attic functionality that is dependant on state"""
    
    def __init__(self, ui, path, patchdir=None):
        """initializes everything, this was copied from mq"""
        self.basepath = path
        self.path = patchdir or os.path.join(path, "attic")
        self.opener = util.opener(self.path)
        self.ui = ui
        self.applied = ''
        self.applied_file = '.applied'
        self.currentpatch = ''
        self.current_file = '.current'
        if not os.path.isdir(self.path):
            try:
                os.mkdir(self.path)
            except OSError, inst:
                if inst.errno != errno.EEXIST or not create:
                    raise
        if os.path.exists(self.join(self.applied_file)):
            self.applied = self.opener(self.applied_file).read().strip()
        if os.path.exists(self.join(self.current_file)):
            self.currentpatch = self.opener(self.current_file).read().strip()
    
    def diffopts(self, opts = {}):
        """proxies a call to patch.diffopts, providing the ui argument"""
        # could this be curried like opener is?
        return patch.diffopts(self.ui, opts)

    def join(self, *p):
        """proxies a call to join, returning paths relative to the attic dir"""
        return os.path.join(self.path, *p)

    def remove(self, patch):
        """removes a patch from the attic dir"""
        os.unlink(self.join(patch))
        
    def available(self):
        """reads all available patches from the attic dir
        
        This method skips all paths that start with a '.' so that you can have
        a repo in the attic dir (just ignore .applied and .currrent). """
        available_list = []
        for root, dirs, files in os.walk(self.path):
            d = root[len(self.path) + 1:]
            for f in files:
                fl = os.path.join(d, f)
                if (not fl.startswith('.')):
                    available_list.append(fl)
        available_list.sort()
        return available_list
    
    def persiststate(self):
        """persists the state of the attic so that you can avoid using the patch name to call commands"""
        fp1 = self.opener(self.applied_file, 'w')
        fp1.write("%s" % self.applied)
        fp1.close()
        fp2 = self.opener(self.current_file, 'w')
        fp2.write("%s" % self.currentpatch)
        fp2.close()
        
    def patch(self, repo, patchfile):
        """actually applies a patch; copied from mq"""
        files = {}
        try:
            fuzz = patch.patch(self.join(patchfile), self.ui, strip=1, cwd=repo.root,
                               files=files)
        except Exception, inst:
            self.ui.note(str(inst) + '\n')
            if not self.ui.verbose:
                self.ui.warn("patch failed, unable to continue (try -v)\n")
            return (False, files, False)

        return (True, files, fuzz)
        
    def check_localchanges(self, repo, force=False):
        """guards against local changes; copied from mq"""
        m, a, r, d = repo.status()[:4]
        if m or a or r or d:
            if not force:
                raise util.Abort(_("local changes found"))
        return m, a, r, d
        
    def apply(self, repo, patch, sim, force=False, **opts):
        """applies a patch and manages repo and attic state"""
        self.check_localchanges(repo,force)
        (success, files, fuzz) = self.patch(repo, patch)
        if success:
            cmdutil.addremove(repo, files, opts, similarity=sim/100.)
            self.applied=patch
            self.currentpatch=patch
            self.persiststate()
            
    def readheaders(self, patch):
        """reads the headers of a patch; copied from mq"""
        def eatdiff(lines):
            while lines:
                l = lines[-1]
                if (l.startswith("diff -") or
                    l.startswith("Index:") or
                    l.startswith("===========")):
                    del lines[-1]
                else:
                    break
        def eatempty(lines):
            while lines:
                l = lines[-1]
                if re.match('\s*$', l):
                    del lines[-1]
                else:
                    break

        pf = self.join(patch)
        message = []
        comments = []
        user = date = format = subject = None
        diffstart = 0
        gitdiff = False

        for line in file(pf):
            line = line.rstrip()
            if line.startswith('diff --git'):
                diffstart = 2
                gitdiff=True
                break
            if diffstart:
                if line.startswith('+++ '):
                    diffstart = 2
                break
            if line.startswith("--- "):
                diffstart = 1
                continue
            elif format == "hgpatch":
                # parse values when importing the result of an hg export
                if line.startswith("# User "):
                    user = line[7:]
                elif line.startswith("# Date "):
                    date = line[7:]
                elif not line.startswith("# ") and line:
                    message.append(line)
                    format = None
            elif line == '# HG changeset patch':
                format = "hgpatch"
            elif (format != "tagdone" and (line.startswith("Subject: ") or
                                           line.startswith("subject: "))):
                subject = line[9:]
                format = "tag"
            elif (format != "tagdone" and (line.startswith("From: ") or
                                           line.startswith("from: "))):
                user = line[6:]
                format = "tag"
            elif format == "tag" and line == "":
                # when looking for tags (subject: from: etc) they
                # end once you find a blank line in the source
                format = "tagdone"
            elif message or line:
                message.append(line)
            comments.append(line)

        eatdiff(message)
        eatdiff(comments)
        eatempty(message)
        eatempty(comments)

        # make sure message isn't empty
        if format and format.startswith("tag") and subject:
            message.insert(0, "")
            message.insert(0, subject)
        return (message, comments, user, date, diffstart > 1, gitdiff)
        
    def createpatch(self, repo, name, msg, user, date, pats=[], opts={}):
        """creates a patch from the current state of the working copy"""
        p = self.opener(name, "w")
        if date:
            p.write("# HG changeset patch\n")
            if user:
                p.write("# User " + user + "\n")
            p.write("# Date %d %d\n" % date)
            p.write("\n")
        elif user:
            p.write("From: " + user + "\n")
            p.write("\n")
        if msg:
            if not isinstance(msg,str):
                msg = "\n".join(msg)
            msg = msg + "\n"
            p.write(msg)
        m = cmdutil.match(repo, pats, opts)
        chunks = patch.diff(repo, match=m, opts=self.diffopts(opts))
        for chunk in chunks:
            p.write(chunk)
        p.close()
        self.currentpatch=name
        self.persiststate()
    
    def cleanup(self, repo):
        """removes all changes from the working copy and makes it so there isn't a patch applied"""
        node = repo.dirstate.parents()[0]
        hg.clean(repo, node, False)
        self.applied=''
        self.persiststate()
        
    def resetdefault(self):
        """resets the default patch (the next command will require a patch name)"""
        self.applied=''
        self.currentpatch=''
        self.persiststate()

def setupheaderopts(ui, opts):
    """sets the user and date; copied from mq"""
    def do(opt,val):
        if not opts.get(opt) and opts.get('current' + opt):
            opts[opt] = val
    do('user', ui.username())
    do('date', "%d %d" % util.makedate())
    
def makepatch(ui, repo, name=None, pats=None, opts=None):
    """sets up the call for attic.createpatch and makes the call"""
    if pats == None:
        pats = []
    if opts == None:
        opts = {}
    
    s = repo.attic
    usr = msg = ''
    d = date = user = None
    force = opts.get('force')
    
    if name and s.applied and name != s.applied and not force:
        raise util.Abort(_("a different patch is active"))
    if not name:
        name = s.applied
    if not name:
        raise util.Abort(_("you need to supply a patch name"))
    message = cmdutil.logmessage(opts)
    if opts.get('edit'):
        message = ui.edit(message, ui.username())
    setupheaderopts(ui, opts)
    if opts.get('user'):
        usr=opts['user']
    if opts.get('date'):
        d=opts['date']
    if s.applied:
        (msg, comments, user, date, hasdiff, gitdiff) = s.readheaders(s.applied)
        if gitdiff:
            opts['git'] = 1
    if message:
        msg=message
    if usr:
        user=usr
    if d:
        date=d
    if date:
        date = util.parsedate(date)
    s.createpatch(repo,name,msg,user,date,pats,opts)
    
def rebuild(ui, repo, name=None, **opts):
    """rebuilds the current active patch with the current changes
    
    This command will act as if you shelved and then unshelved a patch 
    immediately."""
    s = repo.attic
    makepatch(ui,repo,name,[],opts)
    if name:
        s.applied = name
        s.persiststate()
    repo.ui.status(_('Patch %s rebuilt\n') % (s.applied))
  
def shelve(ui, repo, name=None, *pats, **opts):
    """saves a patch to the attic from the current changes and removes them from the working copy"""
    s = repo.attic
    makepatch(ui,repo,name,pats,opts)
    s.cleanup(repo)
    repo.ui.status(_('Patch %s shelved\n' % (s.currentpatch)))
    
def unshelve(ui, repo, name=None, **opts):
    """activates a patch from the attic"""
    s = repo.attic
    force = opts['force']
    if s.applied and not force:
        raise util.Abort(_("cannot apply a patch over an already active patch"))
    if not name:
        name = s.currentpatch
    if not name:
        raise util.Abort(_("patch name must be supplied"))
    try:
        sim = float(opts.get('similarity') or 0)
    except ValueError:
        raise util.Abort(_('similarity must be a number'))
    if sim < 0 or sim > 100:
        raise util.Abort(_('similarity must be between 0 and 100'))
    s.apply(repo, name, sim, opts)
    s.persiststate()
    repo.ui.status(_('Patch %s unshelved\n') % (s.applied))
    
def listattic(ui, repo, **opts):
    """lists the available patches in the attic"""
    available = repo.attic.available()
    repo.ui.write("\n".join(available))
    if len(available) > 0:
        repo.ui.write("\n")

def sfinish(ui, repo, **opts):
    """turns an applied patch into a changeset"""
    s = repo.attic
    name=s.applied
    if not name:
        raise util.Abort(_("no patch active"))
    makepatch(ui, repo, name, [], opts)
    if not opts['nocommit']:
        (m, comments, u, d, hasdiff, gitdiff) = s.readheaders(name)
        commands.commit(ui, repo, message="\n".join(m), logfile=None,
                        user=u, date=d)
                           
    repo.ui.status(_('patch committed\n'))
    if not opts['keep']:
        s.remove(name)
        repo.ui.status(_('patch removed\n'))
    s.resetdefault()

def sactive(ui, repo, **opts):
    """lists the current active patch"""
    s = repo.attic
    active = s.applied
    default = s.currentpatch
    if not active and not default:
        repo.ui.write(_("no patch active or default set\n"))
    elif not active:
        repo.ui.write(_("no patch active; default: %s\n") % (default))
    if active:
        repo.ui.write("%s\n" % (active))
        if opts['header']:
            (message, comments, user, date, hasdiff, gitdiff) = s.readheaders(active)
            if not isinstance(message, str):
                message = "\n".join(message)
            if not message:
                message = None
            else:
                message = "\n" + message
            repo.ui.write(_("\nuser: %s\ndate: %s\nmessage: %s\n") % (user, date, message))
    
def commitwrapper(orig, ui, repo, *args, **opts):
    s = repo.attic
    name=s.applied
    if not name:
        orig(ui, repo, *args, **opts)
    else:
        makepatch(ui, repo, name, [], opts)
        (m, comments, u, d, hasdiff, gitdiff) = s.readheaders(name)
        orig(ui, repo, message="\n".join(m), logfile=None, user=u, date=d)

        if not opts['keep']:
            s.remove(name)
        s.resetdefault()

def reposetup(ui, repo):
    if repo.local():
        repo.attic = attic(ui, repo.join(""))

def uisetup(ui):
    'Replace commit with a decorator to take care of shelves'
    entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper)
    entry[1].append(('k', 'keep', None, _('keep patch file if it is a shelf')))

headeropts = [
    ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
    ('u', 'user', '', _('add "From: <given user>" to patch')),
    ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
    ('d', 'date', '', _('add "Date: <given date>" to patch'))]
                      
cmdtable = {
    "attic-shelve|shelve":   (
        shelve, [
            ('e', 'edit', None, _('edit commit message')),
            ('f', 'force', None, _('force save to file given, overridding pre-existing file')),
            ('g', 'git', None, _('use git extended diff format')),
            ] + commands.walkopts + commands.commitopts + headeropts,
        _('hg attic-shelve [options] [name]')),
    
    "attic-display|attic|ls":   (
        listattic, [], 
        _('hg attic-display')),
    
    "attic-finish|sfinish": (
        sfinish, [
            ('n', 'nocommit', None,_('do not commit')),
            ('e', 'edit', None, _('edit commit message')),
            ('k', 'keep', None, _('keep patch file'))
            ] + commands.commitopts + headeropts, 
        _('hg attic-finish [options]')),
    
    "attic-current|sactive": (
        sactive, [
            ('d', 'header', None, _('display patch header information'))], 
        _('hg attic-current [-d]')),
    
    "attic-unshelve|unshelve": (
        unshelve, [
            ('f', 'force', None, _('force patch over existing changes')),
            ('n', 'dry-run', None,_('do not add or remove, just print output')),
            ('s', 'similarity', '',_('guess renamed files by similarity (0<=s<=100)'))],
        _('hg attic-unshelve [-f] [-n] [-s #] [name]')),
    
    "attic-rebuild|rebuild": (
        rebuild, [
            ('e', 'edit', None, _('edit commit message')),
            ('f', 'force', None, _('force saving patch')),
            ('g', 'git', None, _('use git extended diff format'))
            ] + commands.walkopts + commands.commitopts + headeropts,
        _('hg attic-rebuild [options] [name]'))
}