porcelain.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. # porcelain.py -- Porcelain-like layer on top of Dulwich
  2. # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # or (at your option) a later version of the License.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  17. # MA 02110-1301, USA.
  18. """Simple wrapper that provides porcelain-like functions on top of Dulwich.
  19. Currently implemented:
  20. * archive
  21. * add
  22. * branch{_create,_delete,_list}
  23. * clone
  24. * commit
  25. * commit-tree
  26. * daemon
  27. * diff-tree
  28. * fetch
  29. * init
  30. * pull
  31. * push
  32. * rm
  33. * receive-pack
  34. * reset
  35. * rev-list
  36. * tag{_create,_delete,_list}
  37. * upload-pack
  38. * update-server-info
  39. * status
  40. * symbolic-ref
  41. These functions are meant to behave similarly to the git subcommands.
  42. Differences in behaviour are considered bugs.
  43. """
  44. __docformat__ = 'restructuredText'
  45. from collections import namedtuple
  46. import os
  47. import sys
  48. import time
  49. from dulwich import index
  50. from dulwich.client import get_transport_and_path
  51. from dulwich.errors import (
  52. SendPackError,
  53. UpdateRefsError,
  54. )
  55. from dulwich.index import get_unstaged_changes
  56. from dulwich.objects import (
  57. Tag,
  58. parse_timezone,
  59. )
  60. from dulwich.objectspec import parse_object
  61. from dulwich.patch import write_tree_diff
  62. from dulwich.protocol import Protocol
  63. from dulwich.repo import (BaseRepo, Repo)
  64. from dulwich.server import (
  65. FileSystemBackend,
  66. TCPGitServer,
  67. ReceivePackHandler,
  68. UploadPackHandler,
  69. update_server_info as server_update_server_info,
  70. )
  71. # Module level tuple definition for status output
  72. GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
  73. def open_repo(path_or_repo):
  74. """Open an argument that can be a repository or a path for a repository."""
  75. if isinstance(path_or_repo, BaseRepo):
  76. return path_or_repo
  77. return Repo(path_or_repo)
  78. def archive(location, committish=None, outstream=sys.stdout,
  79. errstream=sys.stderr):
  80. """Create an archive.
  81. :param location: Location of repository for which to generate an archive.
  82. :param committish: Commit SHA1 or ref to use
  83. :param outstream: Output stream (defaults to stdout)
  84. :param errstream: Error stream (defaults to stderr)
  85. """
  86. client, path = get_transport_and_path(location)
  87. if committish is None:
  88. committish = "HEAD"
  89. # TODO(jelmer): This invokes C git; this introduces a dependency.
  90. # Instead, dulwich should have its own archiver implementation.
  91. client.archive(path, committish, outstream.write, errstream.write,
  92. errstream.write)
  93. def update_server_info(repo="."):
  94. """Update server info files for a repository.
  95. :param repo: path to the repository
  96. """
  97. r = open_repo(repo)
  98. server_update_server_info(r)
  99. def symbolic_ref(repo, ref_name, force=False):
  100. """Set git symbolic ref into HEAD.
  101. :param repo: path to the repository
  102. :param ref_name: short name of the new ref
  103. :param force: force settings without checking if it exists in refs/heads
  104. """
  105. repo_obj = open_repo(repo)
  106. ref_path = 'refs/heads/%s' % ref_name
  107. if not force and ref_path not in repo_obj.refs.keys():
  108. raise ValueError('fatal: ref `%s` is not a ref' % ref_name)
  109. repo_obj.refs.set_symbolic_ref('HEAD', ref_path)
  110. def commit(repo=".", message=None, author=None, committer=None):
  111. """Create a new commit.
  112. :param repo: Path to repository
  113. :param message: Optional commit message
  114. :param author: Optional author name and email
  115. :param committer: Optional committer name and email
  116. :return: SHA1 of the new commit
  117. """
  118. # FIXME: Support --all argument
  119. # FIXME: Support --signoff argument
  120. r = open_repo(repo)
  121. return r.do_commit(message=message, author=author,
  122. committer=committer)
  123. def commit_tree(repo, tree, message=None, author=None, committer=None):
  124. """Create a new commit object.
  125. :param repo: Path to repository
  126. :param tree: An existing tree object
  127. :param author: Optional author name and email
  128. :param committer: Optional committer name and email
  129. """
  130. r = open_repo(repo)
  131. return r.do_commit(message=message, tree=tree, committer=committer,
  132. author=author)
  133. def init(path=".", bare=False):
  134. """Create a new git repository.
  135. :param path: Path to repository.
  136. :param bare: Whether to create a bare repository.
  137. :return: A Repo instance
  138. """
  139. if not os.path.exists(path):
  140. os.mkdir(path)
  141. if bare:
  142. return Repo.init_bare(path)
  143. else:
  144. return Repo.init(path)
  145. def clone(source, target=None, bare=False, checkout=None, outstream=sys.stdout):
  146. """Clone a local or remote git repository.
  147. :param source: Path or URL for source repository
  148. :param target: Path to target repository (optional)
  149. :param bare: Whether or not to create a bare repository
  150. :param outstream: Optional stream to write progress to
  151. :return: The new repository
  152. """
  153. if checkout is None:
  154. checkout = (not bare)
  155. if checkout and bare:
  156. raise ValueError("checkout and bare are incompatible")
  157. client, host_path = get_transport_and_path(source)
  158. if target is None:
  159. target = host_path.split("/")[-1]
  160. if not os.path.exists(target):
  161. os.mkdir(target)
  162. if bare:
  163. r = Repo.init_bare(target)
  164. else:
  165. r = Repo.init(target)
  166. remote_refs = client.fetch(host_path, r,
  167. determine_wants=r.object_store.determine_wants_all,
  168. progress=outstream.write)
  169. r["HEAD"] = remote_refs["HEAD"]
  170. if checkout:
  171. outstream.write('Checking out HEAD')
  172. index.build_index_from_tree(r.path, r.index_path(),
  173. r.object_store, r["HEAD"].tree)
  174. return r
  175. def add(repo=".", paths=None):
  176. """Add files to the staging area.
  177. :param repo: Repository for the files
  178. :param paths: Paths to add. No value passed stages all modified files.
  179. """
  180. # FIXME: Support patterns, directories.
  181. r = open_repo(repo)
  182. if not paths:
  183. # If nothing is specified, add all non-ignored files.
  184. paths = []
  185. for dirpath, dirnames, filenames in os.walk(r.path):
  186. # Skip .git and below.
  187. if '.git' in dirnames:
  188. dirnames.remove('.git')
  189. for filename in filenames:
  190. paths.append(os.path.join(dirpath[len(r.path)+1:], filename))
  191. r.stage(paths)
  192. def rm(repo=".", paths=None):
  193. """Remove files from the staging area.
  194. :param repo: Repository for the files
  195. :param paths: Paths to remove
  196. """
  197. r = open_repo(repo)
  198. index = r.open_index()
  199. for p in paths:
  200. del index[p]
  201. index.write()
  202. def print_commit(commit, outstream=sys.stdout):
  203. """Write a human-readable commit log entry.
  204. :param commit: A `Commit` object
  205. :param outstream: A stream file to write to
  206. """
  207. outstream.write("-" * 50 + "\n")
  208. outstream.write("commit: %s\n" % commit.id)
  209. if len(commit.parents) > 1:
  210. outstream.write("merge: %s\n" % "...".join(commit.parents[1:]))
  211. outstream.write("author: %s\n" % commit.author)
  212. outstream.write("committer: %s\n" % commit.committer)
  213. outstream.write("\n")
  214. outstream.write(commit.message + "\n")
  215. outstream.write("\n")
  216. def print_tag(tag, outstream=sys.stdout):
  217. """Write a human-readable tag.
  218. :param tag: A `Tag` object
  219. :param outstream: A stream to write to
  220. """
  221. outstream.write("Tagger: %s\n" % tag.tagger)
  222. outstream.write("Date: %s\n" % tag.tag_time)
  223. outstream.write("\n")
  224. outstream.write("%s\n" % tag.message)
  225. outstream.write("\n")
  226. def show_blob(repo, blob, outstream=sys.stdout):
  227. """Write a blob to a stream.
  228. :param repo: A `Repo` object
  229. :param blob: A `Blob` object
  230. :param outstream: A stream file to write to
  231. """
  232. outstream.write(blob.data)
  233. def show_commit(repo, commit, outstream=sys.stdout):
  234. """Show a commit to a stream.
  235. :param repo: A `Repo` object
  236. :param commit: A `Commit` object
  237. :param outstream: Stream to write to
  238. """
  239. print_commit(commit, outstream)
  240. parent_commit = repo[commit.parents[0]]
  241. write_tree_diff(outstream, repo.object_store, parent_commit.tree, commit.tree)
  242. def show_tree(repo, tree, outstream=sys.stdout):
  243. """Print a tree to a stream.
  244. :param repo: A `Repo` object
  245. :param tree: A `Tree` object
  246. :param outstream: Stream to write to
  247. """
  248. for n in tree:
  249. outstream.write("%s\n" % n)
  250. def show_tag(repo, tag, outstream=sys.stdout):
  251. """Print a tag to a stream.
  252. :param repo: A `Repo` object
  253. :param tag: A `Tag` object
  254. :param outstream: Stream to write to
  255. """
  256. print_tag(tag, outstream)
  257. show_object(repo, repo[tag.object[1]], outstream)
  258. def show_object(repo, obj, outstream):
  259. return {
  260. "tree": show_tree,
  261. "blob": show_blob,
  262. "commit": show_commit,
  263. "tag": show_tag,
  264. }[obj.type_name](repo, obj, outstream)
  265. def log(repo=".", outstream=sys.stdout, max_entries=None):
  266. """Write commit logs.
  267. :param repo: Path to repository
  268. :param outstream: Stream to write log output to
  269. :param max_entries: Optional maximum number of entries to display
  270. """
  271. r = open_repo(repo)
  272. walker = r.get_walker(max_entries=max_entries)
  273. for entry in walker:
  274. print_commit(entry.commit, outstream)
  275. def show(repo=".", objects=None, outstream=sys.stdout):
  276. """Print the changes in a commit.
  277. :param repo: Path to repository
  278. :param objects: Objects to show (defaults to [HEAD])
  279. :param outstream: Stream to write to
  280. """
  281. if objects is None:
  282. objects = ["HEAD"]
  283. if not isinstance(objects, list):
  284. objects = [objects]
  285. r = open_repo(repo)
  286. for objectish in objects:
  287. show_object(r, parse_object(r, objectish), outstream)
  288. def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
  289. """Compares the content and mode of blobs found via two tree objects.
  290. :param repo: Path to repository
  291. :param old_tree: Id of old tree
  292. :param new_tree: Id of new tree
  293. :param outstream: Stream to write to
  294. """
  295. r = open_repo(repo)
  296. write_tree_diff(outstream, r.object_store, old_tree, new_tree)
  297. def rev_list(repo, commits, outstream=sys.stdout):
  298. """Lists commit objects in reverse chronological order.
  299. :param repo: Path to repository
  300. :param commits: Commits over which to iterate
  301. :param outstream: Stream to write to
  302. """
  303. r = open_repo(repo)
  304. for entry in r.get_walker(include=[r[c].id for c in commits]):
  305. outstream.write("%s\n" % entry.commit.id)
  306. def tag(*args, **kwargs):
  307. import warnings
  308. warnings.warn(DeprecationWarning, "tag has been deprecated in favour of tag_create.")
  309. return tag_create(*args, **kwargs)
  310. def tag_create(repo, tag, author=None, message=None, annotated=False,
  311. objectish="HEAD", tag_time=None, tag_timezone=None):
  312. """Creates a tag in git via dulwich calls:
  313. :param repo: Path to repository
  314. :param tag: tag string
  315. :param author: tag author (optional, if annotated is set)
  316. :param message: tag message (optional)
  317. :param annotated: whether to create an annotated tag
  318. :param objectish: object the tag should point at, defaults to HEAD
  319. :param tag_time: Optional time for annotated tag
  320. :param tag_timezone: Optional timezone for annotated tag
  321. """
  322. r = open_repo(repo)
  323. object = parse_object(r, objectish)
  324. if annotated:
  325. # Create the tag object
  326. tag_obj = Tag()
  327. if author is None:
  328. # TODO(jelmer): Don't use repo private method.
  329. author = r._get_user_identity()
  330. tag_obj.tagger = author
  331. tag_obj.message = message
  332. tag_obj.name = tag
  333. tag_obj.object = (type(object), object.id)
  334. tag_obj.tag_time = tag_time
  335. if tag_time is None:
  336. tag_time = int(time.time())
  337. if tag_timezone is None:
  338. # TODO(jelmer) Use current user timezone rather than UTC
  339. tag_timezone = 0
  340. elif isinstance(tag_timezone, str):
  341. tag_timezone = parse_timezone(tag_timezone)
  342. tag_obj.tag_timezone = tag_timezone
  343. r.object_store.add_object(tag_obj)
  344. tag_id = tag_obj.id
  345. else:
  346. tag_id = object.id
  347. r.refs['refs/tags/' + tag] = tag_id
  348. def list_tags(*args, **kwargs):
  349. import warnings
  350. warnings.warn(DeprecationWarning, "list_tags has been deprecated in favour of tag_list.")
  351. return tag_list(*args, **kwargs)
  352. def tag_list(repo, outstream=sys.stdout):
  353. """List all tags.
  354. :param repo: Path to repository
  355. :param outstream: Stream to write tags to
  356. """
  357. r = open_repo(repo)
  358. tags = list(r.refs.as_dict("refs/tags"))
  359. tags.sort()
  360. return tags
  361. def tag_delete(repo, name):
  362. """Remove a tag.
  363. :param repo: Path to repository
  364. :param name: Name of tag to remove
  365. """
  366. r = open_repo(repo)
  367. if isinstance(name, str):
  368. names = [name]
  369. elif isinstance(name, list):
  370. names = name
  371. else:
  372. raise TypeError("Unexpected tag name type %r" % name)
  373. for name in names:
  374. del r.refs["refs/tags/" + name]
  375. def reset(repo, mode, committish="HEAD"):
  376. """Reset current HEAD to the specified state.
  377. :param repo: Path to repository
  378. :param mode: Mode ("hard", "soft", "mixed")
  379. """
  380. if mode != "hard":
  381. raise ValueError("hard is the only mode currently supported")
  382. r = open_repo(repo)
  383. indexfile = r.index_path()
  384. tree = r[committish].tree
  385. index.build_index_from_tree(r.path, indexfile, r.object_store, tree)
  386. def push(repo, remote_location, refs_path,
  387. outstream=sys.stdout, errstream=sys.stderr):
  388. """Remote push with dulwich via dulwich.client
  389. :param repo: Path to repository
  390. :param remote_location: Location of the remote
  391. :param refs_path: relative path to the refs to push to remote
  392. :param outstream: A stream file to write output
  393. :param errstream: A stream file to write errors
  394. """
  395. # Open the repo
  396. r = open_repo(repo)
  397. # Get the client and path
  398. client, path = get_transport_and_path(remote_location)
  399. def update_refs(refs):
  400. new_refs = r.get_refs()
  401. refs[refs_path] = new_refs['HEAD']
  402. del new_refs['HEAD']
  403. return refs
  404. try:
  405. client.send_pack(path, update_refs,
  406. r.object_store.generate_pack_contents, progress=errstream.write)
  407. outstream.write("Push to %s successful.\n" % remote_location)
  408. except (UpdateRefsError, SendPackError) as e:
  409. outstream.write("Push to %s failed.\n" % remote_location)
  410. errstream.write("Push to %s failed -> '%s'\n" % e.message)
  411. def pull(repo, remote_location, refs_path,
  412. outstream=sys.stdout, errstream=sys.stderr):
  413. """Pull from remote via dulwich.client
  414. :param repo: Path to repository
  415. :param remote_location: Location of the remote
  416. :param refs_path: relative path to the fetched refs
  417. :param outstream: A stream file to write to output
  418. :param errstream: A stream file to write to errors
  419. """
  420. # Open the repo
  421. r = open_repo(repo)
  422. client, path = get_transport_and_path(remote_location)
  423. remote_refs = client.fetch(path, r, progress=errstream.write)
  424. r['HEAD'] = remote_refs[refs_path]
  425. # Perform 'git checkout .' - syncs staged changes
  426. indexfile = r.index_path()
  427. tree = r["HEAD"].tree
  428. index.build_index_from_tree(r.path, indexfile, r.object_store, tree)
  429. def status(repo="."):
  430. """Returns staged, unstaged, and untracked changes relative to the HEAD.
  431. :param repo: Path to repository or repository object
  432. :return: GitStatus tuple,
  433. staged - list of staged paths (diff index/HEAD)
  434. unstaged - list of unstaged paths (diff index/working-tree)
  435. untracked - list of untracked, un-ignored & non-.git paths
  436. """
  437. r = open_repo(repo)
  438. # 1. Get status of staged
  439. tracked_changes = get_tree_changes(r)
  440. # 2. Get status of unstaged
  441. unstaged_changes = list(get_unstaged_changes(r.open_index(), r.path))
  442. # TODO - Status of untracked - add untracked changes, need gitignore.
  443. untracked_changes = []
  444. return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
  445. def get_tree_changes(repo):
  446. """Return add/delete/modify changes to tree by comparing index to HEAD.
  447. :param repo: repo path or object
  448. :return: dict with lists for each type of change
  449. """
  450. r = open_repo(repo)
  451. index = r.open_index()
  452. # Compares the Index to the HEAD & determines changes
  453. # Iterate through the changes and report add/delete/modify
  454. # TODO: call out to dulwich.diff_tree somehow.
  455. tracked_changes = {
  456. 'add': [],
  457. 'delete': [],
  458. 'modify': [],
  459. }
  460. for change in index.changes_from_tree(r.object_store, r['HEAD'].tree):
  461. if not change[0][0]:
  462. tracked_changes['add'].append(change[0][1])
  463. elif not change[0][1]:
  464. tracked_changes['delete'].append(change[0][0])
  465. elif change[0][0] == change[0][1]:
  466. tracked_changes['modify'].append(change[0][0])
  467. else:
  468. raise AssertionError('git mv ops not yet supported')
  469. return tracked_changes
  470. def daemon(path=".", address=None, port=None):
  471. """Run a daemon serving Git requests over TCP/IP.
  472. :param path: Path to the directory to serve.
  473. :param address: Optional address to listen on (defaults to ::)
  474. :param port: Optional port to listen on (defaults to TCP_GIT_PORT)
  475. """
  476. # TODO(jelmer): Support git-daemon-export-ok and --export-all.
  477. backend = FileSystemBackend(path)
  478. server = TCPGitServer(backend, address, port)
  479. server.serve_forever()
  480. def web_daemon(path=".", address=None, port=None):
  481. """Run a daemon serving Git requests over HTTP.
  482. :param path: Path to the directory to serve
  483. :param address: Optional address to listen on (defaults to ::)
  484. :param port: Optional port to listen on (defaults to 80)
  485. """
  486. from dulwich.web import (
  487. make_wsgi_chain,
  488. make_server,
  489. WSGIRequestHandlerLogger,
  490. WSGIServerLogger)
  491. backend = FileSystemBackend(path)
  492. app = make_wsgi_chain(backend)
  493. server = make_server(address, port, app,
  494. handler_class=WSGIRequestHandlerLogger,
  495. server_class=WSGIServerLogger)
  496. server.serve_forever()
  497. def upload_pack(path=".", inf=sys.stdin, outf=sys.stdout):
  498. """Upload a pack file after negotiating its contents using smart protocol.
  499. :param path: Path to the repository
  500. :param inf: Input stream to communicate with client
  501. :param outf: Output stream to communicate with client
  502. """
  503. backend = FileSystemBackend()
  504. def send_fn(data):
  505. outf.write(data)
  506. outf.flush()
  507. proto = Protocol(inf.read, send_fn)
  508. handler = UploadPackHandler(backend, [path], proto)
  509. # FIXME: Catch exceptions and write a single-line summary to outf.
  510. handler.handle()
  511. return 0
  512. def receive_pack(path=".", inf=sys.stdin, outf=sys.stdout):
  513. """Receive a pack file after negotiating its contents using smart protocol.
  514. :param path: Path to the repository
  515. :param inf: Input stream to communicate with client
  516. :param outf: Output stream to communicate with client
  517. """
  518. backend = FileSystemBackend()
  519. def send_fn(data):
  520. outf.write(data)
  521. outf.flush()
  522. proto = Protocol(inf.read, send_fn)
  523. handler = ReceivePackHandler(backend, [path], proto)
  524. # FIXME: Catch exceptions and write a single-line summary to outf.
  525. handler.handle()
  526. return 0
  527. def branch_delete(repo, name):
  528. """Delete a branch.
  529. :param repo: Path to the repository
  530. :param name: Name of the branch
  531. """
  532. r = open_repo(repo)
  533. if isinstance(name, str):
  534. names = [name]
  535. elif isinstance(name, list):
  536. names = name
  537. else:
  538. raise TypeError("Unexpected branch name type %r" % name)
  539. for name in names:
  540. del r.refs["refs/heads/" + name]
  541. def branch_create(repo, name, objectish=None, force=False):
  542. """Create a branch.
  543. :param repo: Path to the repository
  544. :param name: Name of the new branch
  545. :param objectish: Target object to point new branch at (defaults to HEAD)
  546. :param force: Force creation of branch, even if it already exists
  547. """
  548. r = open_repo(repo)
  549. if isinstance(name, str):
  550. names = [name]
  551. elif isinstance(name, list):
  552. names = name
  553. else:
  554. raise TypeError("Unexpected branch name type %r" % name)
  555. if objectish is None:
  556. objectish = "HEAD"
  557. object = parse_object(r, objectish)
  558. refname = "refs/heads/" + name
  559. if refname in r.refs and not force:
  560. raise KeyError("Branch with name %s already exists." % name)
  561. r.refs[refname] = object.id
  562. def branch_list(repo):
  563. """List all branches.
  564. :param repo: Path to the repository
  565. """
  566. r = open_repo(repo)
  567. return r.refs.keys(base="refs/heads/")
  568. def fetch(repo, remote_location, outstream=sys.stdout, errstream=sys.stderr):
  569. """Fetch objects from a remote server.
  570. :param repo: Path to the repository
  571. :param remote_location: String identifying a remote server
  572. :param outstream: Output stream (defaults to stdout)
  573. :param errstream: Error stream (defaults to stderr)
  574. :return: Dictionary with refs on the remote
  575. """
  576. r = open_repo(repo)
  577. client, path = get_transport_and_path(remote_location)
  578. remote_refs = client.fetch(path, r, progress=errstream.write)
  579. return remote_refs