porcelain.py 24 KB

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