porcelain.py 27 KB

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