]> arthur.ath.cx Git - bup.git/blob - lib/bup/cmd/get.py
Drop vestigial compat.items
[bup.git] / lib / bup / cmd / get.py
1
2 from __future__ import absolute_import, print_function
3 from binascii import hexlify, unhexlify
4 from collections import namedtuple
5 from stat import S_ISDIR
6 import os, sys, textwrap, time
7
8 from bup import compat, git, client, vfs
9 from bup.compat import (
10     argv_bytes,
11     bytes_from_byte,
12     environ,
13     hexstr
14 )
15 from bup.git import get_cat_data, parse_commit, walk_object
16 from bup.helpers import add_error, debug1, log, saved_errors
17 from bup.helpers import hostname, tty_width, parse_num
18 from bup.io import path_msg
19 from bup.pwdgrp import userfullname, username
20 from bup.repo import LocalRepo, RemoteRepo
21
22 argspec = (
23     "usage: bup get [-s source] [-r remote] (<--ff|--append|...> REF [DEST])...",
24
25     """Transfer data from a source repository to a destination repository
26     according to the methods specified (--ff, --ff:, --append, etc.).
27     Both repositories default to BUP_DIR.  A remote destination may be
28     specified with -r, and data may be pulled from a remote repository
29     with the related "bup on HOST get ..." command.""",
30
31     ('optional arguments:',
32      (('-h, --help', 'show this help message and exit'),
33       ('-v, --verbose',
34        'increase log output (can be specified more than once)'),
35       ('-q, --quiet', "don't show progress meter"),
36       ('-s SOURCE, --source SOURCE',
37        'path to the source repository (defaults to BUP_DIR)'),
38       ('-r REMOTE, --remote REMOTE',
39        'hostname:/path/to/repo of remote destination repository'),
40       ('-t --print-trees', 'output a tree id for each ref set'),
41       ('-c, --print-commits', 'output a commit id for each ref set'),
42       ('--print-tags', 'output an id for each tag'),
43       ('--bwlimit BWLIMIT', 'maximum bytes/sec to transmit to server'),
44       ('-0, -1, -2, -3, -4, -5, -6, -7, -8, -9, --compress LEVEL',
45        'set compression LEVEL (default: 1)'))),
46
47     ('transfer methods:',
48      (('--ff REF, --ff: REF DEST',
49        'fast-forward dest REF (or DEST) to match source REF'),
50       ('--append REF, --append: REF DEST',
51        'append REF (treeish or committish) to dest REF (or DEST)'),
52       ('--pick REF, --pick: REF DEST',
53        'append single source REF commit to dest REF (or DEST)'),
54       ('--force-pick REF, --force-pick: REF DEST',
55        '--pick, overwriting REF (or DEST)'),
56       ('--new-tag REF, --new-tag: REF DEST',
57        'tag source ref REF as REF (or DEST) in dest unless it already exists'),
58       ('--replace, --replace: REF DEST',
59        'overwrite REF (or DEST) in dest with source REF'),
60       ('--unnamed REF',
61        'fetch REF anonymously (without destination ref)'))))
62
63 def render_opts(opts, width=None):
64     if not width:
65         width = tty_width()
66     result = []
67     for args, desc in opts:
68         result.append(textwrap.fill(args, width=width,
69                                     initial_indent=(' ' * 2),
70                                     subsequent_indent=(' ' * 4)))
71         result.append('\n')
72         result.append(textwrap.fill(desc, width=width,
73                                     initial_indent=(' ' * 6),
74                                     subsequent_indent=(' ' * 6)))
75         result.append('\n')
76     return result
77
78 def usage(argspec, width=None):
79     if not width:
80         width = tty_width()
81     usage, preamble, groups = argspec[0], argspec[1], argspec[2:]
82     msg = []
83     msg.append(textwrap.fill(usage, width=width, subsequent_indent='  '))
84     msg.append('\n\n')
85     msg.append(textwrap.fill(preamble.replace('\n', ' '), width=width))
86     msg.append('\n')
87     for group_name, group_args in groups:
88         msg.extend(['\n', group_name, '\n'])
89         msg.extend(render_opts(group_args, width=width))
90     return ''.join(msg)
91
92 def misuse(message=None):
93     sys.stderr.write(usage(argspec))
94     if message:
95         sys.stderr.write('\nerror: ')
96         sys.stderr.write(message)
97         sys.stderr.write('\n')
98     sys.exit(1)
99
100 def require_n_args_or_die(n, args):
101     if len(args) < n + 1:
102         misuse('%s argument requires %d %s'
103                % (n, 'values' if n == 1 else 'value'))
104     result = args[1:1+n], args[1+n:]
105     assert len(result[0]) == n
106     return result
107
108 Spec = namedtuple('Spec', ('method', 'src', 'dest'))
109
110 def spec_msg(s):
111     if not s.dest:
112         return '--%s %s' % (s.method, path_msg(s.src))
113     return '--%s: %s %s' % (s.method, path_msg(s.src), path_msg(s.dest))
114
115 def parse_args(args):
116     class GetOpts:
117         pass
118     opt = GetOpts()
119     opt.help = False
120     opt.verbose = 0
121     opt.quiet = False
122     opt.print_commits = opt.print_trees = opt.print_tags = False
123     opt.bwlimit = None
124     opt.compress = 1
125     opt.source = opt.remote = None
126     opt.target_specs = []
127
128     remaining = args[1:]  # Skip argv[0]
129     while remaining:
130         arg = remaining[0]
131         if arg in (b'-h', b'--help'):
132             sys.stdout.write(usage(argspec))
133             sys.exit(0)
134         elif arg in (b'-v', b'--verbose'):
135             opt.verbose += 1
136             remaining = remaining[1:]
137         elif arg in (b'--ff', b'--append', b'--pick', b'--force-pick',
138                      b'--new-tag', b'--replace', b'--unnamed'):
139             (ref,), remaining = require_n_args_or_die(1, remaining)
140             opt.target_specs.append(Spec(method=arg[2:].decode('ascii'),
141                                          src=ref, dest=None))
142         elif arg in (b'--ff:', b'--append:', b'--pick:', b'--force-pick:',
143                      b'--new-tag:', b'--replace:'):
144             (ref, dest), remaining = require_n_args_or_die(2, remaining)
145             opt.target_specs.append(Spec(method=arg[2:-1].decode('ascii'),
146                                          src=ref, dest=dest))
147         elif arg in (b'-s', b'--source'):
148             (opt.source,), remaining = require_n_args_or_die(1, remaining)
149         elif arg in (b'-r', b'--remote'):
150             (opt.remote,), remaining = require_n_args_or_die(1, remaining)
151         elif arg in (b'-c', b'--print-commits'):
152             opt.print_commits, remaining = True, remaining[1:]
153         elif arg in (b'-t', b'--print-trees'):
154             opt.print_trees, remaining = True, remaining[1:]
155         elif arg == b'--print-tags':
156             opt.print_tags, remaining = True, remaining[1:]
157         elif arg in (b'-0', b'-1', b'-2', b'-3', b'-4', b'-5', b'-6', b'-7',
158                      b'-8', b'-9'):
159             opt.compress = int(arg[1:])
160             remaining = remaining[1:]
161         elif arg == b'--compress':
162             (opt.compress,), remaining = require_n_args_or_die(1, remaining)
163             opt.compress = int(opt.compress)
164         elif arg == b'--bwlimit':
165             (opt.bwlimit,), remaining = require_n_args_or_die(1, remaining)
166             opt.bwlimit = int(opt.bwlimit)
167         elif arg.startswith(b'-') and len(arg) > 2 and arg[1] != b'-':
168             # Try to interpret this as -xyz, i.e. "-xyz -> -x -y -z".
169             # We do this last so that --foo -bar is valid if --foo
170             # requires a value.
171             remaining[0:1] = (b'-' + bytes_from_byte(c) for c in arg[1:])
172             # FIXME
173             continue
174         else:
175             misuse()
176     return opt
177
178 # FIXME: client error handling (remote exceptions, etc.)
179
180 # FIXME: walk_object in in git.py doesn't support opt.verbose.  Do we
181 # need to adjust for that here?
182 def get_random_item(name, hash, repo, writer, opt):
183     def already_seen(oid):
184         return writer.exists(unhexlify(oid))
185     for item in walk_object(repo.cat, hash, stop_at=already_seen,
186                             include_data=True):
187         # already_seen ensures that writer.exists(id) is false.
188         # Otherwise, just_write() would fail.
189         writer.just_write(item.oid, item.type, item.data)
190
191
192 def append_commit(name, hash, parent, src_repo, writer, opt):
193     now = time.time()
194     items = parse_commit(get_cat_data(src_repo.cat(hash), b'commit'))
195     tree = unhexlify(items.tree)
196     author = b'%s <%s>' % (items.author_name, items.author_mail)
197     author_time = (items.author_sec, items.author_offset)
198     committer = b'%s <%s@%s>' % (userfullname(), username(), hostname())
199     get_random_item(name, hexlify(tree), src_repo, writer, opt)
200     c = writer.new_commit(tree, parent,
201                           author, items.author_sec, items.author_offset,
202                           committer, now, None,
203                           items.message)
204     return c, tree
205
206
207 def append_commits(commits, src_name, dest_hash, src_repo, writer, opt):
208     last_c, tree = dest_hash, None
209     for commit in commits:
210         last_c, tree = append_commit(src_name, commit, last_c,
211                                      src_repo, writer, opt)
212     assert(tree is not None)
213     return last_c, tree
214
215 Loc = namedtuple('Loc', ['type', 'hash', 'path'])
216 default_loc = Loc(None, None, None)
217
218 def find_vfs_item(name, repo):
219     res = repo.resolve(name, follow=False, want_meta=False)
220     leaf_name, leaf_item = res[-1]
221     if not leaf_item:
222         return None
223     kind = type(leaf_item)
224     if kind == vfs.Root:
225         kind = 'root'
226     elif kind == vfs.Tags:
227         kind = 'tags'
228     elif kind == vfs.RevList:
229         kind = 'branch'
230     elif kind == vfs.Commit:
231         if len(res) > 1 and isinstance(res[-2][1], vfs.RevList):
232             kind = 'save'
233         else:
234             kind = 'commit'
235     elif kind == vfs.Item:
236         if S_ISDIR(vfs.item_mode(leaf_item)):
237             kind = 'tree'
238         else:
239             kind = 'blob'
240     elif kind == vfs.Chunky:
241         kind = 'tree'
242     elif kind == vfs.FakeLink:
243         # Don't have to worry about ELOOP, excepting malicious
244         # remotes, since "latest" is the only FakeLink.
245         assert leaf_name == b'latest'
246         res = repo.resolve(leaf_item.target, parent=res[:-1],
247                            follow=False, want_meta=False)
248         leaf_name, leaf_item = res[-1]
249         assert leaf_item
250         assert isinstance(leaf_item, vfs.Commit)
251         name = b'/'.join(x[0] for x in res)
252         kind = 'save'
253     else:
254         raise Exception('unexpected resolution for %s: %r'
255                         % (path_msg(name), res))
256     path = b'/'.join(name for name, item in res)
257     if hasattr(leaf_item, 'coid'):
258         result = Loc(type=kind, hash=leaf_item.coid, path=path)
259     elif hasattr(leaf_item, 'oid'):
260         result = Loc(type=kind, hash=leaf_item.oid, path=path)
261     else:
262         result = Loc(type=kind, hash=None, path=path)
263     return result
264
265
266 Target = namedtuple('Target', ['spec', 'src', 'dest'])
267
268 def loc_desc(loc):
269     if loc and loc.hash:
270         loc = loc._replace(hash=hexlify(loc.hash))
271     return repr(loc)
272
273
274 # FIXME: see if resolve() means we can drop the vfs path cleanup
275
276 def cleanup_vfs_path(p):
277     result = os.path.normpath(p)
278     if result.startswith(b'/'):
279         return result
280     return b'/' + result
281
282
283 def validate_vfs_path(p, spec):
284     if p.startswith(b'/.') \
285        and not p.startswith(b'/.tag/'):
286         misuse('unsupported destination path %s in %s'
287                % (path_msg(p), spec_msg(spec)))
288     return p
289
290
291 def resolve_src(spec, src_repo):
292     src = find_vfs_item(spec.src, src_repo)
293     spec_args = spec_msg(spec)
294     if not src:
295         misuse('cannot find source for %s' % spec_args)
296     if src.type == 'root':
297         misuse('cannot fetch entire repository for %s' % spec_args)
298     if src.type == 'tags':
299         misuse('cannot fetch entire /.tag directory for %s' % spec_args)
300     debug1('src: %s\n' % loc_desc(src))
301     return src
302
303
304 def get_save_branch(repo, path):
305     res = repo.resolve(path, follow=False, want_meta=False)
306     leaf_name, leaf_item = res[-1]
307     if not leaf_item:
308         misuse('error: cannot access %r in %r' % (leaf_name, path))
309     assert len(res) == 3
310     res_path = b'/'.join(name for name, item in res[:-1])
311     return res_path
312
313
314 def resolve_branch_dest(spec, src, src_repo, dest_repo):
315     # Resulting dest must be treeish, or not exist.
316     if not spec.dest:
317         # Pick a default dest.
318         if src.type == 'branch':
319             spec = spec._replace(dest=spec.src)
320         elif src.type == 'save':
321             spec = spec._replace(dest=get_save_branch(src_repo, spec.src))
322         elif src.path.startswith(b'/.tag/'):  # Dest defaults to the same.
323             spec = spec._replace(dest=spec.src)
324
325     spec_args = spec_msg(spec)
326     if not spec.dest:
327         misuse('no destination (implicit or explicit) for %s', spec_args)
328
329     dest = find_vfs_item(spec.dest, dest_repo)
330     if dest:
331         if dest.type == 'commit':
332             misuse('destination for %s is a tagged commit, not a branch'
333                   % spec_args)
334         if dest.type != 'branch':
335             misuse('destination for %s is a %s, not a branch'
336                   % (spec_args, dest.type))
337     else:
338         dest = default_loc._replace(path=cleanup_vfs_path(spec.dest))
339
340     if dest.path.startswith(b'/.'):
341         misuse('destination for %s must be a valid branch name' % spec_args)
342
343     debug1('dest: %s\n' % loc_desc(dest))
344     return spec, dest
345
346
347 def resolve_ff(spec, src_repo, dest_repo):
348     src = resolve_src(spec, src_repo)
349     spec_args = spec_msg(spec)
350     if src.type == 'tree':
351         misuse('%s is impossible; can only --append a tree to a branch'
352               % spec_args)
353     if src.type not in ('branch', 'save', 'commit'):
354         misuse('source for %s must be a branch, save, or commit, not %s'
355               % (spec_args, src.type))
356     spec, dest = resolve_branch_dest(spec, src, src_repo, dest_repo)
357     return Target(spec=spec, src=src, dest=dest)
358
359
360 def handle_ff(item, src_repo, writer, opt):
361     assert item.spec.method == 'ff'
362     assert item.src.type in ('branch', 'save', 'commit')
363     src_oidx = hexlify(item.src.hash)
364     dest_oidx = hexlify(item.dest.hash) if item.dest.hash else None
365     if not dest_oidx or dest_oidx in src_repo.rev_list(src_oidx):
366         # Can fast forward.
367         get_random_item(item.spec.src, src_oidx, src_repo, writer, opt)
368         commit_items = parse_commit(get_cat_data(src_repo.cat(src_oidx), b'commit'))
369         return item.src.hash, unhexlify(commit_items.tree)
370     misuse('destination is not an ancestor of source for %s'
371            % spec_msg(item.spec))
372     # misuse() doesn't return
373     return None
374
375
376 def resolve_append(spec, src_repo, dest_repo):
377     src = resolve_src(spec, src_repo)
378     if src.type not in ('branch', 'save', 'commit', 'tree'):
379         misuse('source for %s must be a branch, save, commit, or tree, not %s'
380               % (spec_msg(spec), src.type))
381     spec, dest = resolve_branch_dest(spec, src, src_repo, dest_repo)
382     return Target(spec=spec, src=src, dest=dest)
383
384
385 def handle_append(item, src_repo, writer, opt):
386     assert item.spec.method == 'append'
387     assert item.src.type in ('branch', 'save', 'commit', 'tree')
388     assert item.dest.type == 'branch' or not item.dest.type
389     src_oidx = hexlify(item.src.hash)
390     if item.src.type == 'tree':
391         get_random_item(item.spec.src, src_oidx, src_repo, writer, opt)
392         parent = item.dest.hash
393         msg = b'bup save\n\nGenerated by command:\n%r\n' % compat.get_argvb()
394         userline = b'%s <%s@%s>' % (userfullname(), username(), hostname())
395         now = time.time()
396         commit = writer.new_commit(item.src.hash, parent,
397                                    userline, now, None,
398                                    userline, now, None, msg)
399         return commit, item.src.hash
400     commits = list(src_repo.rev_list(src_oidx))
401     commits.reverse()
402     return append_commits(commits, item.spec.src, item.dest.hash,
403                           src_repo, writer, opt)
404
405
406 def resolve_pick(spec, src_repo, dest_repo):
407     src = resolve_src(spec, src_repo)
408     spec_args = spec_msg(spec)
409     if src.type == 'tree':
410         misuse('%s is impossible; can only --append a tree' % spec_args)
411     if src.type not in ('commit', 'save'):
412         misuse('%s impossible; can only pick a commit or save, not %s'
413               % (spec_args, src.type))
414     if not spec.dest:
415         if src.path.startswith(b'/.tag/'):
416             spec = spec._replace(dest=spec.src)
417         elif src.type == 'save':
418             spec = spec._replace(dest=get_save_branch(src_repo, spec.src))
419     if not spec.dest:
420         misuse('no destination provided for %s', spec_args)
421     dest = find_vfs_item(spec.dest, dest_repo)
422     if not dest:
423         cp = validate_vfs_path(cleanup_vfs_path(spec.dest), spec)
424         dest = default_loc._replace(path=cp)
425     else:
426         if not dest.type == 'branch' and not dest.path.startswith(b'/.tag/'):
427             misuse('%s destination is not a tag or branch' % spec_args)
428         if spec.method == 'pick' \
429            and dest.hash and dest.path.startswith(b'/.tag/'):
430             misuse('cannot overwrite existing tag for %s (requires --force-pick)'
431                   % spec_args)
432     return Target(spec=spec, src=src, dest=dest)
433
434
435 def handle_pick(item, src_repo, writer, opt):
436     assert item.spec.method in ('pick', 'force-pick')
437     assert item.src.type in ('save', 'commit')
438     src_oidx = hexlify(item.src.hash)
439     if item.dest.hash:
440         return append_commit(item.spec.src, src_oidx, item.dest.hash,
441                              src_repo, writer, opt)
442     return append_commit(item.spec.src, src_oidx, None, src_repo, writer, opt)
443
444
445 def resolve_new_tag(spec, src_repo, dest_repo):
446     src = resolve_src(spec, src_repo)
447     spec_args = spec_msg(spec)
448     if not spec.dest and src.path.startswith(b'/.tag/'):
449         spec = spec._replace(dest=src.path)
450     if not spec.dest:
451         misuse('no destination (implicit or explicit) for %s', spec_args)
452     dest = find_vfs_item(spec.dest, dest_repo)
453     if not dest:
454         dest = default_loc._replace(path=cleanup_vfs_path(spec.dest))
455     if not dest.path.startswith(b'/.tag/'):
456         misuse('destination for %s must be a VFS tag' % spec_args)
457     if dest.hash:
458         misuse('cannot overwrite existing tag for %s (requires --replace)'
459               % spec_args)
460     return Target(spec=spec, src=src, dest=dest)
461
462
463 def handle_new_tag(item, src_repo, writer, opt):
464     assert item.spec.method == 'new-tag'
465     assert item.dest.path.startswith(b'/.tag/')
466     get_random_item(item.spec.src, hexlify(item.src.hash),
467                     src_repo, writer, opt)
468     return (item.src.hash,)
469
470
471 def resolve_replace(spec, src_repo, dest_repo):
472     src = resolve_src(spec, src_repo)
473     spec_args = spec_msg(spec)
474     if not spec.dest:
475         if src.path.startswith(b'/.tag/') or src.type == 'branch':
476             spec = spec._replace(dest=spec.src)
477     if not spec.dest:
478         misuse('no destination provided for %s', spec_args)
479     dest = find_vfs_item(spec.dest, dest_repo)
480     if dest:
481         if not dest.type == 'branch' and not dest.path.startswith(b'/.tag/'):
482             misuse('%s impossible; can only overwrite branch or tag'
483                   % spec_args)
484     else:
485         cp = validate_vfs_path(cleanup_vfs_path(spec.dest), spec)
486         dest = default_loc._replace(path=cp)
487     if not dest.path.startswith(b'/.tag/') \
488        and not src.type in ('branch', 'save', 'commit'):
489         misuse('cannot overwrite branch with %s for %s' % (src.type, spec_args))
490     return Target(spec=spec, src=src, dest=dest)
491
492
493 def handle_replace(item, src_repo, writer, opt):
494     assert(item.spec.method == 'replace')
495     if item.dest.path.startswith(b'/.tag/'):
496         get_random_item(item.spec.src, hexlify(item.src.hash),
497                         src_repo, writer, opt)
498         return (item.src.hash,)
499     assert(item.dest.type == 'branch' or not item.dest.type)
500     src_oidx = hexlify(item.src.hash)
501     get_random_item(item.spec.src, src_oidx, src_repo, writer, opt)
502     commit_items = parse_commit(get_cat_data(src_repo.cat(src_oidx), b'commit'))
503     return item.src.hash, unhexlify(commit_items.tree)
504
505
506 def resolve_unnamed(spec, src_repo, dest_repo):
507     if spec.dest:
508         misuse('destination name given for %s' % spec_msg(spec))
509     src = resolve_src(spec, src_repo)
510     return Target(spec=spec, src=src, dest=None)
511
512
513 def handle_unnamed(item, src_repo, writer, opt):
514     get_random_item(item.spec.src, hexlify(item.src.hash),
515                     src_repo, writer, opt)
516     return (None,)
517
518
519 def resolve_targets(specs, src_repo, dest_repo):
520     resolved_items = []
521     common_args = src_repo, dest_repo
522     for spec in specs:
523         debug1('initial-spec: %r\n' % (spec,))
524         if spec.method == 'ff':
525             resolved_items.append(resolve_ff(spec, *common_args))
526         elif spec.method == 'append':
527             resolved_items.append(resolve_append(spec, *common_args))
528         elif spec.method in ('pick', 'force-pick'):
529             resolved_items.append(resolve_pick(spec, *common_args))
530         elif spec.method == 'new-tag':
531             resolved_items.append(resolve_new_tag(spec, *common_args))
532         elif spec.method == 'replace':
533             resolved_items.append(resolve_replace(spec, *common_args))
534         elif spec.method == 'unnamed':
535             resolved_items.append(resolve_unnamed(spec, *common_args))
536         else: # Should be impossible -- prevented by the option parser.
537             assert(False)
538
539     # FIXME: check for prefix overlap?  i.e.:
540     #   bup get --ff foo --ff: baz foo/bar
541     #   bup get --new-tag .tag/foo --new-tag: bar .tag/foo/bar
542
543     # Now that we have all the items, check for duplicate tags.
544     tags_targeted = set()
545     for item in resolved_items:
546         dest_path = item.dest and item.dest.path
547         if dest_path:
548             assert(dest_path.startswith(b'/'))
549             if dest_path.startswith(b'/.tag/'):
550                 if dest_path in tags_targeted:
551                     if item.spec.method not in ('replace', 'force-pick'):
552                         misuse('cannot overwrite tag %s via %s' \
553                               % (path_msg(dest_path), spec_msg(item.spec)))
554                 else:
555                     tags_targeted.add(dest_path)
556     return resolved_items
557
558
559 def log_item(name, type, opt, tree=None, commit=None, tag=None):
560     if tag and opt.print_tags:
561         print(hexstr(tag))
562     if tree and opt.print_trees:
563         print(hexstr(tree))
564     if commit and opt.print_commits:
565         print(hexstr(commit))
566     if opt.verbose:
567         last = ''
568         if type in ('root', 'branch', 'save', 'commit', 'tree'):
569             if not name.endswith(b'/'):
570                 last = '/'
571         log('%s%s\n' % (path_msg(name), last))
572
573 def main(argv):
574     is_reverse = environ.get(b'BUP_SERVER_REVERSE')
575     opt = parse_args(argv)
576     git.check_repo_or_die()
577     if opt.source:
578         opt.source = argv_bytes(opt.source)
579     if opt.bwlimit:
580         client.bwlimit = parse_num(opt.bwlimit)
581     if is_reverse and opt.remote:
582         misuse("don't use -r in reverse mode; it's automatic")
583     if opt.remote or is_reverse:
584         dest_repo = RemoteRepo(opt.remote)
585     else:
586         dest_repo = LocalRepo()
587
588     with dest_repo as dest_repo:
589         with LocalRepo(repo_dir=opt.source) as src_repo:
590             with dest_repo.new_packwriter(compression_level=opt.compress) as writer:
591                 # Resolve and validate all sources and destinations,
592                 # implicit or explicit, and do it up-front, so we can
593                 # fail before we start writing (for any obviously
594                 # broken cases).
595                 target_items = resolve_targets(opt.target_specs,
596                                                src_repo, dest_repo)
597
598                 updated_refs = {}  # ref_name -> (original_ref, tip_commit(bin))
599                 no_ref_info = (None, None)
600
601                 handlers = {'ff': handle_ff,
602                             'append': handle_append,
603                             'force-pick': handle_pick,
604                             'pick': handle_pick,
605                             'new-tag': handle_new_tag,
606                             'replace': handle_replace,
607                             'unnamed': handle_unnamed}
608
609                 for item in target_items:
610                     debug1('get-spec: %r\n' % (item.spec,))
611                     debug1('get-src: %s\n' % loc_desc(item.src))
612                     debug1('get-dest: %s\n' % loc_desc(item.dest))
613                     dest_path = item.dest and item.dest.path
614                     if dest_path:
615                         if dest_path.startswith(b'/.tag/'):
616                             dest_ref = b'refs/tags/%s' % dest_path[6:]
617                         else:
618                             dest_ref = b'refs/heads/%s' % dest_path[1:]
619                     else:
620                         dest_ref = None
621
622                     dest_hash = item.dest and item.dest.hash
623                     orig_ref, cur_ref = updated_refs.get(dest_ref, no_ref_info)
624                     orig_ref = orig_ref or dest_hash
625                     cur_ref = cur_ref or dest_hash
626
627                     handler = handlers[item.spec.method]
628                     item_result = handler(item, src_repo, writer, opt)
629                     if len(item_result) > 1:
630                         new_id, tree = item_result
631                     else:
632                         new_id = item_result[0]
633
634                     if not dest_ref:
635                         log_item(item.spec.src, item.src.type, opt)
636                     else:
637                         updated_refs[dest_ref] = (orig_ref, new_id)
638                         if dest_ref.startswith(b'refs/tags/'):
639                             log_item(item.spec.src, item.src.type, opt, tag=new_id)
640                         else:
641                             log_item(item.spec.src, item.src.type, opt,
642                                      tree=tree, commit=new_id)
643
644         # Only update the refs at the very end, once the writer is
645         # closed, so that if something goes wrong above, the old refs
646         # will be undisturbed.
647         for ref_name, info in updated_refs.items():
648             orig_ref, new_ref = info
649             try:
650                 dest_repo.update_ref(ref_name, new_ref, orig_ref)
651                 if opt.verbose:
652                     new_hex = hexlify(new_ref)
653                     if orig_ref:
654                         orig_hex = hexlify(orig_ref)
655                         log('updated %r (%s -> %s)\n' % (ref_name, orig_hex, new_hex))
656                     else:
657                         log('updated %r (%s)\n' % (ref_name, new_hex))
658             except (git.GitError, client.ClientError) as ex:
659                 add_error('unable to update ref %r: %s' % (ref_name, ex))
660
661     if saved_errors:
662         log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))
663         sys.exit(1)