porcelain.py 31 KB

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