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