Source

aurum / autoload / aurum / drivers / bazaar.vim

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
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
"▶1
scriptencoding utf-8
let s:pp='aurum.aubazaar'
execute frawor#Setup('0.0', {'@%aurum/drivers/common/hypsites': '0.0',
            \                                   '@%aurum/repo': '5.0',
            \                   '@%aurum/drivers/common/utils': '1.2',
            \                                           '@/os': '0.1',
            \                                      '@/options': '0.0',})
let s:_messages={
            \   'revnof': 'Failed to get working directory revision '.
            \             'from the repository %s: %s',
            \     'logf': 'Failed to list all revisions '.
            \             'in the repository %s: %s',
            \      'csf': 'Failed to get information about revision %s '.
            \             'in the repository %s: %s',
            \      'lsf': 'Failed to list files in the changeset %s '.
            \             'from the repository %s: %s',
            \'lsignoref': 'Failed to list ignored files '.
            \             'in the repository %s: %s',
            \   'labelf': 'Failed to set %s “%s” for the changeset %s '.
            \             'in the repository %s: %s',
            \  'revertf': 'Failet to revert to the changeset %s '.
            \             'in the repository %s: %s',
            \  'updatef': 'Failed to update to the changeset %s '.
            \             'in the repository %s: %s',
            \      'mvf': 'Failed to move %s to %s in the repository %s: %s',
            \     'addf': 'Failed to add file %s to the repository %s: %s',
            \      'fgf': 'Failed to forget file %s in the repository %s: %s',
            \      'rmf': 'Failed to removed file %s in the repository %s: %s',
            \  'ignoref': 'Failed to ignore %s in the repository %s: %s',
            \     'catf': 'Failed to get file %s in the changeset %s '.
            \             'of the repository %s: %s',
            \'annotatef': 'Failed to annotate file %s in the changeset %s '.
            \             'of the repository %s: %s',
            \    'difff': 'Failed to get diff between %s and %s for files %s '.
            \             'in the repository %s: %s',
            \    'nickf': 'Failed to get branch name for the repository %s: %s',
            \ 'nicksetf': 'Failed to set nick %s for the repository %s: %s',
            \  'configf': 'Failed to get parent_location '.
            \             'of the repository %s: %s',
            \     'tagf': 'Failed to list tags in the repository %s: %s',
            \    'pushf': 'Failed to push the repository %s: %s',
            \    'pullf': 'Failed to pull to the repository %s: %s',
            \  'commitf': 'Failed to commit changes to the repository %s: %s',
            \  'p_empty': 'Parser error: expected 60 dashes, but got nothing',
            \  'p_nobeg': 'Parser error: expected 60 dashes, but got %s',
            \ 'p_nospec': 'Parser error: expected “spec: value”, but got %s',
            \'p_dateerr': 'Parser error: date exited with code %u '.
            \             'while trying to parse “%s”: %s',
            \    'ndate': 'You must install “date” programm in order to get '.
            \             'time information for bazaar revisions',
            \     'rene': 'Failed to get rename information: '.
            \             'no “ => ” in string %s',
            \    'renze': 'Failed to get rename information: '.
            \             '“ => ” found at the start of the string %s',
            \   'renz2e': 'Failed to get rename information: '.
            \             '“ => ” found at the end of the string %s',
            \   'cbnimp': 'Bazaar driver is not able to close branch',
            \  'upfnimp': 'Bazaar driver is not able to force update',
            \  'revnimp': 'In order to get reverse diff you must specify '.
            \             'both revisions',
            \   'bfnimp': 'Can’t force branch nick creation',
            \   'drnimp': 'Dry run not implemented',
            \    'nocfg': 'Can’t get property %s of the repository %s',
        \}
let s:bzr={}
let s:_options={
        \}
"▶1 s:hypsites
" TODO Support for git and subversion hypsites
let s:hypsites=s:_r.hypsites.bzr
"▶1 bzrcmd :: cmd, args, kwargs → String
function s:F.bzrcmd(...)
    return ['bzr', '--no-aliases']+call(s:_r.utils.getcmd, a:000, {})
endfunction
"▶1 bzr :: repo, cmd, args, kwargs, has0[, msgid[, marg1[, …]]] → [String] + ?
function s:F.bzr(repo, cmd, args, kwargs, hasnulls, ...)
    let cmd=s:F.bzrcmd(a:cmd, a:args, a:kwargs)
    let [r, exit_code]=s:_r.utils.run(cmd, a:hasnulls, a:repo.path)
    if a:0
        if a:1 is 0
            return [r, exit_code]
        elseif exit_code
            call call(s:_f.throw, a:000+[a:repo.path, join(r[:-1-(a:hasnulls)],
                        \                                  "\n")], {})
        endif
    endif
    return r
endfunction
"▶1 bzrm :: {bzr args} → + :echom
function s:F.bzrm(...)
    return s:_r.utils.printm(call(s:F.bzr, a:000, {}))
endfunction
"▶1 parsecs :: csdata, lstart::UInt → (cs, line::UInt)
" hash-parent hashes-timestamp
"  (refs)
" author name
" author email
" 1-indented commit message
let s:logkwargs={'long': 1, 'show-ids': 1}
let s:hasdateexe=executable('date')
function s:F.parsecs(csdata, ...)
    if empty(a:csdata)
        call s:_f.throw('p_empty')
    elseif a:csdata[0]!~#'\v^\s*\-{60}$'
        call s:_f.throw('p_nobeg', a:csdata[0])
    endif
    let indent=stridx(remove(a:csdata, 0), '-')
    " FIXME children:[]
    let cs={'parents': [], 'copies': {}, 'children': [], 'phase': 'unknown',
                \'bookmarks': [], 'tags': [], 'branch': 'default'}
    while !empty(a:csdata) && a:csdata[0]!~#'\v^\s*\-{60}$'
        let line=remove(a:csdata, 0)[(indent):]
        if line is# 'message:'
            let description=[]
            " XXX message can have line “----”
            let regex='\v^('.((indent)?
                        \       (join(map(range(0, indent/4),
                        \                 '"\\s{".(v:val*4)."}"'), '|')):
                        \       ('')).'|\s{'.(indent+4).'})\-{60}$'
            while !empty(a:csdata) && a:csdata[0] !~# regex
                call add(description, remove(a:csdata, 0)[(indent+2):])
            endwhile
            let cs.description=join(description, "\n")
            break
        endif
        let match=matchlist(line, '\v^([^:]+)\: ?(.*)$')[1:2]
        if empty(match)
            call s:_f.throw('p_nospec', line)
        endif
        let [spec, value]=match
        if spec is# 'revno'
            let cs.rev=matchstr(value, '\v^\S+')
        elseif spec is# 'revision-id'
            if a:0 && has_key(a:1, value)
                let spaces=repeat(' ', indent-1)
                while a:csdata[:(indent-1)] is# spaces
                    call remove(a:csdata, 0)
                endwhile
                return {'hex': value}
            else
                let cs.hex=value
            endif
        elseif spec is# 'parent'
            let cs.parents+=[value]
        elseif spec is# 'author'
            let cs.user=value
        elseif spec is# 'committer' && !has_key(cs, 'user')
            let cs.user=value
        elseif spec is# 'branch nick'
            let cs.branch=value
        elseif spec is# 'tags'
            " FIXME There may be “, ” in the tag, use “bzr tags” to fix it
            " XXX Can’t fix with “bzr tags” completely: tag may contain spaces 
            "     at the end
            let cs.tags=split(value, ', ')
        elseif spec is# 'timestamp'
            if s:hasdateexe || executable('date')
                let s:hasdateexe=1
                let time=system('date --date='.shellescape(value).' +%s')
                if v:shell_error
                    call s:_f.throw('p_dateerr', v:shell_error, value, time)
                endif
                let cs.time=+time
            else
                call s:_f.warn('ndate')
                let cs.time=0
            endif
        endif
    endwhile
    return cs
endfunction
"▶1 bzr.getcs :: repo, rev → cs
let s:gcslogkwargs=extend({'levels': '1'}, s:logkwargs)
function s:bzr.getcs(repo, rev)
    let kwargs=copy(s:gcslogkwargs)
    let kwargs.revision=''.a:rev
    let log=s:F.bzr(a:repo, 'log', [], kwargs, 0, 'csf', a:rev)
    let cs=s:F.parsecs(log)
    " XXX This construct is used to preserve information like “allfiles” etc
    let a:repo.changesets[cs.hex]=extend(get(a:repo.changesets, cs.hex, {}), cs)
    return a:repo.changesets[cs.hex]
endfunction
"▶1 bzr.getwork :: repo → cs
function s:bzr.getwork(repo)
    let rev=+s:F.bzr(a:repo, 'revno', [], {'tree': 1}, 0, 'revnof')[0]
    return a:repo.functions.getcs(a:repo, rev)
endfunction
"▶1 bzr.getchangesets :: repo[, rangestart, rangeend] → [cs]
let s:gcsslogkwargs=extend({'levels': '0'}, s:logkwargs)
function s:bzr.getchangesets(repo, ...)
    let kwargs=copy(s:gcsslogkwargs)
    if a:0
        let kwargs.revision=a:1.'..'.a:2
    endif
    let log=s:F.bzr(a:repo, 'log', [], kwargs, 0, 'logf')
    let cslist=[]
    let csmap={}
    while !empty(log)
        let cs=s:F.parsecs(log, csmap)
        if has_key(csmap, cs.hex)
            let pos=remove(csmap, cs.hex)
            call map(csmap, '(v:val>'.pos.')?(v:val-1):(v:val)')
            let cs=remove(cslist, pos)
        else
            let a:repo.changesets[cs.hex]=cs
        endif
        let csmap[cs.hex]=len(cslist)
        call insert(cslist, cs)
    endwhile
    return cslist
endfunction
"▶1 bzr.revrange :: repo, rev1, rev2 → [cs]
let s:bzr.revrange=s:bzr.getchangesets
"▶1 bzr.updatechangesets :: repo → _
function s:bzr.updatechangesets(...)
    " FIXME Get new changesets
endfunction
"▶1 bzr.getrevhex :: repo, rev → hex
let s:prevrevhex={}
function s:bzr.getrevhex(repo, rev)
    return a:repo.functions.getcs(a:repo, a:rev).hex
endfunction
"▶1 bzr.getworkhex :: repo → hex
function s:bzr.getworkhex(repo)
    return a:repo.functions.getwork(a:repo).hex
endfunction
"▶1 bzr.gettiphex :: repo → hex
function s:bzr.gettiphex(repo)
    return a:repo.functions.getrevhex(a:repo, '-1')
endfunction
"▶1 getstatdict :: repo, args, kwargs → statdict
let s:emptystatdict=extend({'renamed': []}, deepcopy(s:_r.utils.emptystatdct))
function s:F.getstatdict(repo, args, kwargs)
    let lines=s:F.bzr(a:repo, 'status', a:args,
                \      extend({'no-classify': 1, 'no-pending': 1, 'short': 1},
                \             a:kwargs),
                \      0)[:-2]
    let curstatus=0
    let statdict=deepcopy(s:emptystatdict)
    while !empty(lines)
        let line=remove(lines, 0)
        let status=line[:2]
        if (status!~#'^[ +\-?RIXCP][ KNMD!][ *]$' || line[3] isnot# ' ')
                    \&& curstatus isnot 0
            let statdict[curstatus][-1].=line
        endif
        let file=line[4:]
        if status[0] is# 'R'
            let statdict.renamed+=[file]
            let curstatus='renamed'
        elseif status[0] is# ' '
            if status[1] is# 'N'
                let statdict.added    += [file]
                let curstatus='added'
            elseif stridx('D!', status[1])!=-1
                let statdict.deleted  += [file]
                let curstatus='deleted'
            elseif status[2] is# '*' || stridx('KM', status[1])!=-1
                let statdict.modified += [file]
                let curstatus='modified'
            else
                let statdict.clean    += [file]
                let curstatus='clean'
            endif
        elseif status[0] is# '-'
            let statdict.removed+=[file]
            let curstatus='removed'
        elseif status[0] is# '+'
            if status[1] is# '!'
                let statdict.deleted  += [file]
                let curstatus='deleted'
            else
                let statdict.added    += [file]
                let curstatus='added'
            endif
        elseif status[0] is# '?'
            let statdict.unknown+=[file]
            let curstatus='unknown'
        elseif status[0] is# 'I'
            let statdict.ignored+=[file]
            let curstatus='ignored'
        " elseif status[0] is# 'X'
            " Nonexistent file
        endif
    endwhile
    return statdict
endfunction
"▶1 getrenames :: statdict → renames
function s:F.getrenames(statdict)
    if !has_key(a:statdict, 'renamed')
        return {}
    endif
    let renames={}
    for rename in a:statdict.renamed
        let idx=stridx(rename, " => ")
        if idx==-1
            call s:_f.throw('rene', rename)
        elseif idx==0
            call s:_f.throw('renze', rename)
        endif
        let old=rename[:(idx-1)]
        let new=rename[idx+4:]
        try
            let renames[new]=old
        catch /^Vim(let):E713:/
            call s:_f.throw('renz2e', rename)
        endtry
    endfor
    return renames
endfunction
"▶1 bzr.setcsprop :: repo, cs, propname → propvalue
function s:bzr.setcsprop(repo, cs, prop)
    if a:prop is# 'allfiles'
        let r=s:_r.utils.nullnl(
                    \s:F.bzr(a:repo, 'ls', [], {'revision': 'revid:'.a:cs.hex,
                    \                          'recursive': 1,
                    \                               'null': 1}, 2,
                    \        'lsf', a:cs.hex))[:-2]
    elseif       a:prop is# 'renames' || a:prop is# 'changes' ||
                \a:prop is# 'files'   || a:prop is# 'removes'
        let statdict=s:F.getstatdict(a:repo, [], {'change': 'revid:'.a:cs.hex})
        let a:cs.removes  = copy(statdict.removed)
        let a:cs.files    = statdict.added+statdict.modified
        let a:cs.renames  = s:F.getrenames(statdict)
        let a:cs.removes += sort(values(a:cs.renames))
        let a:cs.files   += sort(keys(  a:cs.renames))
        let a:cs.changes  = a:cs.removes+a:cs.files
        return a:cs[a:prop]
    endif
    let a:cs[a:prop]=r
    return r
endfunction
"▶1 bzr.status :: repo[, rev1[, rev2[, files[, clean[, ign]]]]] → {type:[file]}
if s:usepythondriver "▶2
let s:revargsexpr='v:val is 0? '.
                \       '"None":'.
                \ 'v:key>=3?'.
                \       '(empty(v:val)?"False":"True"):'.
                \       '"vim.eval(''a:".(v:key+1)."'')"'
function s:bzr.status(repo, ...)
    let revargs=join(map(copy(a:000), s:revargsexpr), ',')
    let d={}
    try
        execute s:pya.'get_status(vim.eval("a:repo.path"), '.revargs.')'
    endtry
    return d
endfunction
else "▶2
function s:bzr.status(repo, ...)
    let args=['--']+((a:0>2 && a:3 isnot 0)?(a:3):([]))
    let statdict=s:F.getstatdict(a:repo, args,
                \                ((a:0>1 && a:1 isnot 0 && a:2 isnot 0)?
                \                   ({'revision': a:1.'..'.a:2}):
                \                ((a:0>0 && a:1 isnot 0)?
                \                   ({'revision': a:1.'..'}):
                \                ((a:0>1 && a:2 isnot 0)?
                \                   ({'revision': a:2.'..'}):
                \                   ({})))))
    let r=deepcopy(statdict)
    let renames=s:F.getrenames(statdict)
    call remove(r, 'renamed')
    call filter(r.modified, '!has_key(renames, v:val)')
    if a:0>2 && !empty(a:3)
        let r.added   += sort(filter(keys(renames),   'index(a:3, v:val)!=-1'))
        let r.removed += sort(filter(values(renames), 'index(a:3, v:val)!=-1'))
    else
        let r.added   += sort(keys(renames))
        let r.removed += sort(values(renames))
    endif
    if a:0>1 && a:1 is 0 && a:2 isnot 0
        let [r.deleted, r.unknown]=[r.unknown, r.deleted]
        let [r.added,   r.removed]=[r.removed, r.added  ]
    endif
    if a:0>3 && a:4
        if a:1 is 0
            if a:2 is 0
                let rev=a:repo.functions.getworkhex(a:repo)
            else
                let rev=a:2
            endif
        else
            let rev=a:1
        endif
        let allfiles=copy(a:repo.functions.getcsprop(a:repo, rev, 'allfiles'))
        if !empty(a:3)
            let allfiles=filter(allfiles, 'index(a:3, v:val)!=-1')
        endif
        let files=r.modified+r.added+r.removed+r.deleted+r.unknown
        let r.clean=filter(allfiles, 'index(files, v:val)==-1')
    endif
    " Ignored files are listed in “bzr status” output, but only if they are 
    " specified on the command-line. Bazaar really has good documentation:
    " “bzr help status” does not have anything about “missing”, “ignored” and 
    " “nonexistent” statuses or different behavior for explicitely specified 
    " paths.
    if (a:0>4 && a:5 && a:1 is 0 && a:2 is 0 && empty(a:3))
        let r.ignored=s:_r.utils.nullnl(
                    \ s:F.bzr(a:repo, 'ls', args, {'ignored': 1, 'null': 1}, 2,
                    \                 'lsignoref', a:repo))[:-2]
    endif
    return r
endfunction
endif
"▶1 bzr.commit :: repo, message[, files[, user[, date[, _]]]]
function s:bzr.commit(repo, message, ...)
    let kwargs={}
    let args=[]
    if a:0
        if !empty(a:1)
            let args+=['--']+a:1
            call s:_r.utils.addfiles(a:repo, a:1)
        endif
        if a:0>1 && !empty(a:2)
            let kwargs.author=a:2
        endif
        if a:0>2 && !empty(a:3)
            let kwargs['commit-time']=a:3.' +0000'
        endif
        if a:0>3 && !empty(a:4)
            call s:_f.throw('cbnimp')
        endif
    endif
    return s:_r.utils.usefile(a:repo, a:message, 'file', 'message',
                \             s:F.bzrm, args, kwargs, 0, 'commitf')
endfunction
"▶1 bzr.branch :: repo, branchname, force → + FS
function s:bzr.branch(repo, branch, force)
    if a:force
        call s:_f.throw('bfnimp')
    endif
    return s:F.bzrm(a:repo, 'nick', ['--', a:branch], {}, 0,
                \   'nicksetf', a:branch)
endfunction
"▶1 bzr.label :: repo, type, label, rev, force, local → + FS
function s:bzr.label(repo, type, label, rev, force, local)
    if a:local
        call s:_f.throw('nloc')
    endif
    let args=['--', a:label]
    let kwargs={}
    if a:force
        let kwargs.force=1
    endif
    if a:rev is 0
        let kwargs.delete=1
    else
        let kwargs.revision=''.a:rev
    endif
    return s:F.bzrm(a:repo, a:type, args, kwargs, 0,
                \   'labelf', a:type, a:label, a:rev)
endfunction
"▶1 bzr.update :: repo, rev, force → + FS
function s:bzr.update(repo, rev, force)
    let kwargs={}
    let kwargs.revision=''.a:rev
    if a:force
        call s:F.bzrm(a:repo, 'revert', [], kwargs, 0, 'revertf', a:rev)
    endif
    return s:F.bzrm(a:repo, 'update', [], kwargs, 0, 'updatef', a:rev)
endfunction
"▶1 bzr.move :: repo, force, source, destination → + FS
function s:bzr.move(repo, force, source, destination)
    return s:F.bzrm(a:repo, 'mv', ['--', a:source, a:destination], {}, 0,
                \   'mvf', a:source, a:destination)
endfunction
"▶1 bzr.add :: repo, file → + FS
function s:bzr.add(repo, file)
    return s:F.bzrm(a:repo, 'add', ['--', a:file], {}, 0, 'addf', a:file)
endfunction
"▶1 bzr.forget :: repo, file → + FS
function s:bzr.forget(repo, file)
    return s:F.bzrm(a:repo, 'rm', ['--', a:file], {'keep': 1}, 0,
                \   'fgf', a:file)
endfunction
"▶1 bzr.remove :: repo, file → + FS
function s:bzr.remove(repo, file)
    return s:F.bzrm(a:repo, 'rm', ['--', a:file], {'no-backup': 1}, 0,
                \   'rmf', a:file)
endfunction
"▶1 bzr.ignore :: repo, file → + FS
function s:bzr.ignore(repo, file)
    return a:repo.functions.ignoreglob(a:repo,
                \'RE:'.substitute(a:file, '\W', '\\\0', 'g'))
endfunction
"▶1 bzr.ignoreglob :: repo, glob → + FS
function s:bzr.ignoreglob(repo, glob)
    return s:F.bzrm(a:repo, 'ignore', ['--', a:glob], {}, 0, 'ignoref', a:glob)
endfunction
"▶1 bzr.readfile :: repo, rev, file → [String]
function s:bzr.readfile(repo, rev, file)
    return s:F.bzr(a:repo, 'cat', ['--', a:file], {'revision': ''.a:rev}, 2,
                \  'catf', a:rev, a:file)
endfunction
"▶1 bzr.annotate :: repo, rev, file → [(file, hex, linenumber)]
" XXX Bazaar supports annotating current state, it uses id=current:
function s:bzr.annotate(repo, rev, file)
    let ann=s:F.bzr(a:repo, 'annotate', ['--', a:file],
                \   {'rev': a:rev, 'show-ids': 1, 'all': 1}, 2,
                \   'annotatef', a:file, a:rev)[:-2]
    let lastid=0
    let r=[]
    let idamap={}
    for line in ann
        let id=line[match(line, '\S'):(stridx(line, '|')-2)]
        let r+=[[id, 0]]
        let idamap[id]=get(idamap, id, [])+[r[-1]]
    endfor
    " XXX Bazaar annotation respects renames, but it does not show old filename, 
    "     thus rename information is to be used
    for [id, lists] in items(idamap)
        let renames=s:F.getrenames(s:F.getstatdict(a:repo, ['--', a:file],
                    \                              {'revision': a:rev.'..'.id}))
        let file=get(renames, a:file, a:file)
        call map(lists, 'insert(v:val, '.string(file).')')
    endfor
    return r
endfunction
"▶1 bzr.diff :: repo, rev, rev, files, opts → [String]
let s:difftrans={
            \ 'numlines': 'unified',
            \ 'ignorews': 'ignore-all-space',
            \'iwsamount': 'ignore-space-change',
            \  'iblanks': 'ignore-blank-lines',
            \ 'showfunc': 'show-c-function',
            \  'alltext': 'text',
        \}
function s:bzr.diff(repo, rev1, rev2, files, opts)
    let reverse=get(a:opts, 'reverse', 0)
    let diffopts=s:_r.utils.diffopts(a:opts, a:repo.diffopts, s:difftrans)
    let kwargs={}
    if !empty(diffopts)
        let kwargs['diff-options']=join(s:_r.utils.kwargstolst(diffopts))
    endif
    let args=[]
    if empty(a:rev2)
        if reverse
            call s:_f.throw('revnimp')
        endif
        if !empty(a:rev1)
            let kwargs.change=''.a:rev1
        endif
    else
        if reverse
            if empty(a:rev1)
                call s:_f.throw('revnimp')
            endif
            let kwargs.revision=a:rev1.'..'.a:rev2
        else
            let kwargs.revision=''.a:rev2
            if !empty(a:rev1)
                let kwargs.revision.='..'.a:rev1
            endif
        endif
    endif
    if !empty(a:files)
        let args+=['--']+a:files
    endif
    let [r, exit_code]=s:F.bzr(a:repo, 'diff', args, kwargs, 1, 0)
    " 0 is returned when repository is unchanged
    " 1 is returned when repository has changes
    " 2 and further indicate an error
    if exit_code>1
        call s:_f.throw('difff', a:rev1, a:rev2, join(a:files, ', '),
                    \            a:repo.path, join(r, "\n"))
    endif
    return r
endfunction
"▶1 bzr.diffre :: _, opts → Regex
let s:diffre='\m^=== \v(\a+)\ file\ (.*)'
function s:bzr.diffre(repo, opts)
    return s:diffre
endfunction
"▶1 bzr.diffname :: _, line, diffre, _ → rpath
function s:bzr.diffname(repo, line, diffre, opts)
    let match=matchlist(a:line, a:diffre)[1:2]
    if empty(match)
        return 0
    elseif match[0] is# 'renamed'
        return matchstr(match[1], '\v%(\''.*\''\V => ''\v)@<=.*%(\''$)@=')
    else
        return match[1][1:-2]
    endif
endfunction
"▶1 bzr.getrepoprop :: repo, propname → a
function s:bzr.getrepoprop(repo, prop)
    if a:prop is# 'branch'
        return join(s:F.bzr(a:repo, 'nick', [], {}, 0, 'nickf')[:-2], "\n")
    elseif a:prop is# 'url'
        return s:F.bzr(a:repo, 'config', ['parent_location'], {}, 0,
                    \  'configf')[:-2]
    elseif a:prop is# 'brancheslist'
        return []
        " FIXME Use “bzr branches”?
    elseif a:prop is# 'tagslist'
        return map(s:F.bzr(a:repo, 'tags', [], {}, 0, 'tagf')[:-2],
                    \'substitute(v:val, '' \+\S\+$'', "", "")')
    elseif a:prop is# 'bookmarkslist'
        return []
    endif
    call s:_f.throw('nocfg', a:prop, a:repo.path)
endfunction
"▶1 bzr.push :: repo, dryrun, force[, URL[, rev]]
function s:bzr.push(repo, dryrun, force, ...)
    if a:dryrun
        call s:_f.throw('drnimp')
    endif
    let args=[]
    let kwargs={}
    if a:force
        let kwargs.overwrite=1
        let kwargs['use-existing-dir']=1
        let kwargs['create-prefix']=1
    endif
    if a:0 && a:1 isnot 0
        let args+=['--', a:1]
    endif
    if a:0>1 && a:2 isnot 0
        let kwargs.revision=''.a:2
    endif
    return s:F.bzrm(a:repo, 'push', args, kwargs, 0, 'pushf')
endfunction
"▶1 bzr.pull :: repo, dryrun, force[, URL[, rev]]
function s:bzr.pull(repo, dryrun, force, ...)
    if a:dryrun
        call s:_f.throw('drnimp')
    endif
    let args=[]
    let kwargs={}
    if a:force
        let kwargs.overwrite=1
    endif
    if a:0 && a:1 isnot 0
        let args+=['--', a:1]
    endif
    if a:0>1 && a:2 isnot 0
        let kwargs.revision=''.a:2
    endif
    return s:F.bzrm(a:repo, 'pull', args, kwargs, 0, 'pullf')
endfunction
"▶1 bzr.repo :: path → repo
function s:bzr.repo(path)
    let repo={'path': a:path, 'changesets': {}, 'mutable': {},
                \'local': (stridx(a:path, '://')==-1),
                \'labeltypes': ['tag'],
                \'revreg': '\v\d+(\.\d+)*',
                \'hexreg': '\v[a-zA-Z0-9_.\-+]+\@[a-zA-Z0-9_.\-+]+',
                \'hasrevisions': 1, 'requires_sort': 0,
                \'hypsites': deepcopy(s:hypsites),
                \}
    return repo
endfunction
"▶1 bzr.checkdir :: dir → Bool
function s:bzr.checkdir(dir)
    return s:_r.os.path.isdir(s:_r.os.path.join(a:dir, '.bzr'))
endfunction
"▶1 TODO iterfuncs
" TODO
"▶1 astatus, agetcs, agetrepoprop
if s:_r.repo.userepeatedcmd
    try
        python import aurum.rcdriverfuncs
        let s:addafuncs=1
    catch
        let s:addafuncs=0
    endtry
    if s:addafuncs
        function s:bzr.astatus(repo, interval, ...)
            if a:0<3 || a:1 isnot 0 || a:2 isnot 0 ||
                        \type(a:3)!=type([]) || len(a:3)!=1
                call s:_f.throw('aconimp')
            endif
            return s:_r.utils.pyeval('aurum.repeatedcmd.new('.
                        \        string(a:interval).', '.
                        \       'aurum.rcdriverfuncs.bzr_status, '.
                        \        s:_r.utils.pystring(a:repo.path).', '.
                        \        s:_r.utils.pystring(a:3[0]).')')
        endfunction
        function s:bzr.agetrepoprop(repo, interval, prop)
            if a:prop isnot# 'branch'
                call s:_f.throw('anbnimp')
            endif
            return s:_r.utils.pyeval('aurum.repeatedcmd.new('.
                        \        string(a:interval).', '.
                        \       'aurum.rcdriverfuncs.bzr_branch, '.
                        \        s:_r.utils.pystring(a:repo.path).')')
        endfunction
    endif
endif
"▶1 Register driver
call s:_f.regdriver('Bazaar', s:bzr)
"▶1
call frawor#Lockvar(s:, 'hasdateexe')
" vim: ft=vim ts=4 sts=4 et fmr=▶,▲