porcelain.py 32 KB

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