porcelain.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  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. * check-ignore
  26. * checkout
  27. * clone
  28. * commit
  29. * commit-tree
  30. * daemon
  31. * diff-tree
  32. * fetch
  33. * init
  34. * ls-remote
  35. * ls-tree
  36. * pull
  37. * push
  38. * rm
  39. * remote{_add}
  40. * receive-pack
  41. * reset
  42. * rev-list
  43. * tag{_create,_delete,_list}
  44. * upload-pack
  45. * update-server-info
  46. * status
  47. * symbolic-ref
  48. These functions are meant to behave similarly to the git subcommands.
  49. Differences in behaviour are considered bugs.
  50. Functions should generally accept both unicode strings and bytestrings
  51. """
  52. from collections import namedtuple
  53. from contextlib import (
  54. closing,
  55. contextmanager,
  56. )
  57. from io import BytesIO
  58. import os
  59. import posixpath
  60. import stat
  61. import sys
  62. import time
  63. from dulwich.archive import (
  64. tar_stream,
  65. )
  66. from dulwich.client import (
  67. get_transport_and_path,
  68. )
  69. from dulwich.config import (
  70. StackedConfig,
  71. )
  72. from dulwich.diff_tree import (
  73. CHANGE_ADD,
  74. CHANGE_DELETE,
  75. CHANGE_MODIFY,
  76. CHANGE_RENAME,
  77. CHANGE_COPY,
  78. RENAME_CHANGE_TYPES,
  79. )
  80. from dulwich.errors import (
  81. SendPackError,
  82. UpdateRefsError,
  83. )
  84. from dulwich.ignore import IgnoreFilterManager
  85. from dulwich.index import (
  86. blob_from_path_and_stat,
  87. get_unstaged_changes,
  88. )
  89. from dulwich.object_store import (
  90. tree_lookup_path,
  91. )
  92. from dulwich.objects import (
  93. Commit,
  94. Tag,
  95. format_timezone,
  96. parse_timezone,
  97. pretty_format_tree_entry,
  98. )
  99. from dulwich.objectspec import (
  100. parse_commit,
  101. parse_object,
  102. parse_ref,
  103. parse_reftuples,
  104. parse_tree,
  105. )
  106. from dulwich.pack import (
  107. write_pack_index,
  108. write_pack_objects,
  109. )
  110. from dulwich.patch import write_tree_diff
  111. from dulwich.protocol import (
  112. Protocol,
  113. ZERO_SHA,
  114. )
  115. from dulwich.refs import ANNOTATED_TAG_SUFFIX
  116. from dulwich.repo import (BaseRepo, Repo)
  117. from dulwich.server import (
  118. FileSystemBackend,
  119. TCPGitServer,
  120. ReceivePackHandler,
  121. UploadPackHandler,
  122. update_server_info as server_update_server_info,
  123. )
  124. # Module level tuple definition for status output
  125. GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
  126. default_bytes_out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
  127. default_bytes_err_stream = getattr(sys.stderr, 'buffer', sys.stderr)
  128. DEFAULT_ENCODING = 'utf-8'
  129. class RemoteExists(Exception):
  130. """Raised when the remote already exists."""
  131. def open_repo(path_or_repo):
  132. """Open an argument that can be a repository or a path for a repository."""
  133. if isinstance(path_or_repo, BaseRepo):
  134. return path_or_repo
  135. return Repo(path_or_repo)
  136. @contextmanager
  137. def _noop_context_manager(obj):
  138. """Context manager that has the same api as closing but does nothing."""
  139. yield obj
  140. def open_repo_closing(path_or_repo):
  141. """Open an argument that can be a repository or a path for a repository.
  142. returns a context manager that will close the repo on exit if the argument
  143. is a path, else does nothing if the argument is a repo.
  144. """
  145. if isinstance(path_or_repo, BaseRepo):
  146. return _noop_context_manager(path_or_repo)
  147. return closing(Repo(path_or_repo))
  148. def path_to_tree_path(repopath, path):
  149. """Convert a path to a path usable in e.g. an index.
  150. :param repo: Repository
  151. :param path: A path
  152. :return: A path formatted for use in e.g. an index
  153. """
  154. os.path.relpath(path, repopath)
  155. if os.path.sep != '/':
  156. path = path.replace(os.path.sep, '/')
  157. return path.encode(sys.getfilesystemencoding())
  158. def archive(repo, committish=None, outstream=default_bytes_out_stream,
  159. errstream=default_bytes_err_stream):
  160. """Create an archive.
  161. :param repo: Path of repository for which to generate an archive.
  162. :param committish: Commit SHA1 or ref to use
  163. :param outstream: Output stream (defaults to stdout)
  164. :param errstream: Error stream (defaults to stderr)
  165. """
  166. if committish is None:
  167. committish = "HEAD"
  168. with open_repo_closing(repo) as repo_obj:
  169. c = repo_obj[committish]
  170. for chunk in tar_stream(
  171. repo_obj.object_store, repo_obj.object_store[c.tree],
  172. c.commit_time):
  173. outstream.write(chunk)
  174. def update_server_info(repo="."):
  175. """Update server info files for a repository.
  176. :param repo: path to the repository
  177. """
  178. with open_repo_closing(repo) as r:
  179. server_update_server_info(r)
  180. def symbolic_ref(repo, ref_name, force=False):
  181. """Set git symbolic ref into HEAD.
  182. :param repo: path to the repository
  183. :param ref_name: short name of the new ref
  184. :param force: force settings without checking if it exists in refs/heads
  185. """
  186. with open_repo_closing(repo) as repo_obj:
  187. ref_path = _make_branch_ref(ref_name)
  188. if not force and ref_path not in repo_obj.refs.keys():
  189. raise ValueError('fatal: ref `%s` is not a ref' % ref_name)
  190. repo_obj.refs.set_symbolic_ref(b'HEAD', ref_path)
  191. def commit(repo=".", message=None, author=None, committer=None, encoding=None):
  192. """Create a new commit.
  193. :param repo: Path to repository
  194. :param message: Optional commit message
  195. :param author: Optional author name and email
  196. :param committer: Optional committer name and email
  197. :return: SHA1 of the new commit
  198. """
  199. # FIXME: Support --all argument
  200. # FIXME: Support --signoff argument
  201. if getattr(message, 'encode', None):
  202. message = message.encode(encoding or DEFAULT_ENCODING)
  203. if getattr(author, 'encode', None):
  204. author = author.encode(encoding or DEFAULT_ENCODING)
  205. if getattr(committer, 'encode', None):
  206. committer = committer.encode(encoding or DEFAULT_ENCODING)
  207. with open_repo_closing(repo) as r:
  208. return r.do_commit(
  209. message=message, author=author, committer=committer,
  210. encoding=encoding)
  211. def commit_tree(repo, tree, message=None, author=None, committer=None):
  212. """Create a new commit object.
  213. :param repo: Path to repository
  214. :param tree: An existing tree object
  215. :param author: Optional author name and email
  216. :param committer: Optional committer name and email
  217. """
  218. with open_repo_closing(repo) as r:
  219. return r.do_commit(
  220. message=message, tree=tree, committer=committer, author=author)
  221. def init(path=".", bare=False):
  222. """Create a new git repository.
  223. :param path: Path to repository.
  224. :param bare: Whether to create a bare repository.
  225. :return: A Repo instance
  226. """
  227. if not os.path.exists(path):
  228. os.mkdir(path)
  229. if bare:
  230. return Repo.init_bare(path)
  231. else:
  232. return Repo.init(path)
  233. def clone(source, target=None, bare=False, checkout=None,
  234. errstream=default_bytes_err_stream, outstream=None,
  235. origin=b"origin", **kwargs):
  236. """Clone a local or remote git repository.
  237. :param source: Path or URL for source repository
  238. :param target: Path to target repository (optional)
  239. :param bare: Whether or not to create a bare repository
  240. :param checkout: Whether or not to check-out HEAD after cloning
  241. :param errstream: Optional stream to write progress to
  242. :param outstream: Optional stream to write progress to (deprecated)
  243. :param origin: Name of remote from the repository used to clone
  244. :return: The new repository
  245. """
  246. if outstream is not None:
  247. import warnings
  248. warnings.warn(
  249. "outstream= has been deprecated in favour of errstream=.",
  250. DeprecationWarning, stacklevel=3)
  251. errstream = outstream
  252. if checkout is None:
  253. checkout = (not bare)
  254. if checkout and bare:
  255. raise ValueError("checkout and bare are incompatible")
  256. config = StackedConfig.default()
  257. client, host_path = get_transport_and_path(source, config=config, **kwargs)
  258. if target is None:
  259. target = host_path.split("/")[-1]
  260. if not os.path.exists(target):
  261. os.mkdir(target)
  262. if bare:
  263. r = Repo.init_bare(target)
  264. else:
  265. r = Repo.init(target)
  266. try:
  267. fetch_result = client.fetch(
  268. host_path, r, determine_wants=r.object_store.determine_wants_all,
  269. progress=errstream.write)
  270. ref_message = b"clone: from " + source.encode('utf-8')
  271. r.refs.import_refs(
  272. b'refs/remotes/' + origin,
  273. {n[len(b'refs/heads/'):]: v for (n, v) in fetch_result.refs.items()
  274. if n.startswith(b'refs/heads/')},
  275. message=ref_message)
  276. r.refs.import_refs(
  277. b'refs/tags',
  278. {n[len(b'refs/tags/'):]: v for (n, v) in fetch_result.refs.items()
  279. if n.startswith(b'refs/tags/') and
  280. not n.endswith(ANNOTATED_TAG_SUFFIX)},
  281. message=ref_message)
  282. target_config = r.get_config()
  283. if not isinstance(source, bytes):
  284. source = source.encode(DEFAULT_ENCODING)
  285. target_config.set((b'remote', origin), b'url', source)
  286. target_config.set(
  287. (b'remote', origin), b'fetch',
  288. b'+refs/heads/*:refs/remotes/' + origin + b'/*')
  289. target_config.write_to_path()
  290. # TODO(jelmer): Support symref capability,
  291. # https://github.com/jelmer/dulwich/issues/485
  292. try:
  293. head = r[fetch_result.refs[b"HEAD"]]
  294. except KeyError:
  295. head = None
  296. else:
  297. r[b'HEAD'] = head.id
  298. if checkout and not bare and head is not None:
  299. errstream.write(b'Checking out ' + head.id + b'\n')
  300. r.reset_index(head.tree)
  301. except BaseException:
  302. r.close()
  303. raise
  304. return r
  305. def add(repo=".", paths=None):
  306. """Add files to the staging area.
  307. :param repo: Repository for the files
  308. :param paths: Paths to add. No value passed stages all modified files.
  309. :return: Tuple with set of added files and ignored files
  310. """
  311. ignored = set()
  312. with open_repo_closing(repo) as r:
  313. ignore_manager = IgnoreFilterManager.from_repo(r)
  314. if not paths:
  315. paths = list(
  316. get_untracked_paths(os.getcwd(), r.path, r.open_index()))
  317. relpaths = []
  318. if not isinstance(paths, list):
  319. paths = [paths]
  320. for p in paths:
  321. relpath = os.path.relpath(p, r.path)
  322. if relpath.startswith('../'):
  323. raise ValueError('path %r is not in repo' % relpath)
  324. # FIXME: Support patterns, directories.
  325. if ignore_manager.is_ignored(relpath):
  326. ignored.add(relpath)
  327. continue
  328. relpaths.append(relpath)
  329. r.stage(relpaths)
  330. return (relpaths, ignored)
  331. def remove(repo=".", paths=None, cached=False):
  332. """Remove files from the staging area.
  333. :param repo: Repository for the files
  334. :param paths: Paths to remove
  335. """
  336. with open_repo_closing(repo) as r:
  337. index = r.open_index()
  338. for p in paths:
  339. full_path = os.path.abspath(p).encode(sys.getfilesystemencoding())
  340. tree_path = path_to_tree_path(r.path, p)
  341. try:
  342. index_sha = index[tree_path].sha
  343. except KeyError:
  344. raise Exception('%s did not match any files' % p)
  345. if not cached:
  346. try:
  347. st = os.lstat(full_path)
  348. except OSError:
  349. pass
  350. else:
  351. try:
  352. blob = blob_from_path_and_stat(full_path, st)
  353. except IOError:
  354. pass
  355. else:
  356. try:
  357. committed_sha = tree_lookup_path(
  358. r.__getitem__, r[r.head()].tree, tree_path)[1]
  359. except KeyError:
  360. committed_sha = None
  361. if blob.id != index_sha and index_sha != committed_sha:
  362. raise Exception(
  363. 'file has staged content differing '
  364. 'from both the file and head: %s' % p)
  365. if index_sha != committed_sha:
  366. raise Exception(
  367. 'file has staged changes: %s' % p)
  368. os.remove(full_path)
  369. del index[tree_path]
  370. index.write()
  371. rm = remove
  372. def commit_decode(commit, contents, default_encoding=DEFAULT_ENCODING):
  373. if commit.encoding is not None:
  374. return contents.decode(commit.encoding, "replace")
  375. return contents.decode(default_encoding, "replace")
  376. def print_commit(commit, decode, outstream=sys.stdout):
  377. """Write a human-readable commit log entry.
  378. :param commit: A `Commit` object
  379. :param outstream: A stream file to write to
  380. """
  381. outstream.write("-" * 50 + "\n")
  382. outstream.write("commit: " + commit.id.decode('ascii') + "\n")
  383. if len(commit.parents) > 1:
  384. outstream.write(
  385. "merge: " +
  386. "...".join([c.decode('ascii') for c in commit.parents[1:]]) + "\n")
  387. outstream.write("Author: " + decode(commit.author) + "\n")
  388. if commit.author != commit.committer:
  389. outstream.write("Committer: " + decode(commit.committer) + "\n")
  390. time_tuple = time.gmtime(commit.author_time + commit.author_timezone)
  391. time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple)
  392. timezone_str = format_timezone(commit.author_timezone).decode('ascii')
  393. outstream.write("Date: " + time_str + " " + timezone_str + "\n")
  394. outstream.write("\n")
  395. outstream.write(decode(commit.message) + "\n")
  396. outstream.write("\n")
  397. def print_tag(tag, decode, outstream=sys.stdout):
  398. """Write a human-readable tag.
  399. :param tag: A `Tag` object
  400. :param decode: Function for decoding bytes to unicode string
  401. :param outstream: A stream to write to
  402. """
  403. outstream.write("Tagger: " + decode(tag.tagger) + "\n")
  404. outstream.write("Date: " + decode(tag.tag_time) + "\n")
  405. outstream.write("\n")
  406. outstream.write(decode(tag.message) + "\n")
  407. outstream.write("\n")
  408. def show_blob(repo, blob, decode, outstream=sys.stdout):
  409. """Write a blob to a stream.
  410. :param repo: A `Repo` object
  411. :param blob: A `Blob` object
  412. :param decode: Function for decoding bytes to unicode string
  413. :param outstream: A stream file to write to
  414. """
  415. outstream.write(decode(blob.data))
  416. def show_commit(repo, commit, decode, outstream=sys.stdout):
  417. """Show a commit to a stream.
  418. :param repo: A `Repo` object
  419. :param commit: A `Commit` object
  420. :param decode: Function for decoding bytes to unicode string
  421. :param outstream: Stream to write to
  422. """
  423. print_commit(commit, decode=decode, outstream=outstream)
  424. if commit.parents:
  425. parent_commit = repo[commit.parents[0]]
  426. base_tree = parent_commit.tree
  427. else:
  428. base_tree = None
  429. diffstream = BytesIO()
  430. write_tree_diff(
  431. diffstream,
  432. repo.object_store, base_tree, commit.tree)
  433. diffstream.seek(0)
  434. outstream.write(
  435. diffstream.getvalue().decode(
  436. commit.encoding or DEFAULT_ENCODING, 'replace'))
  437. def show_tree(repo, tree, decode, outstream=sys.stdout):
  438. """Print a tree to a stream.
  439. :param repo: A `Repo` object
  440. :param tree: A `Tree` object
  441. :param decode: Function for decoding bytes to unicode string
  442. :param outstream: Stream to write to
  443. """
  444. for n in tree:
  445. outstream.write(decode(n) + "\n")
  446. def show_tag(repo, tag, decode, outstream=sys.stdout):
  447. """Print a tag to a stream.
  448. :param repo: A `Repo` object
  449. :param tag: A `Tag` object
  450. :param decode: Function for decoding bytes to unicode string
  451. :param outstream: Stream to write to
  452. """
  453. print_tag(tag, decode, outstream)
  454. show_object(repo, repo[tag.object[1]], outstream)
  455. def show_object(repo, obj, decode, outstream):
  456. return {
  457. b"tree": show_tree,
  458. b"blob": show_blob,
  459. b"commit": show_commit,
  460. b"tag": show_tag,
  461. }[obj.type_name](repo, obj, decode, outstream)
  462. def print_name_status(changes):
  463. """Print a simple status summary, listing changed files.
  464. """
  465. for change in changes:
  466. if not change:
  467. continue
  468. if isinstance(change, list):
  469. change = change[0]
  470. if change.type == CHANGE_ADD:
  471. path1 = change.new.path
  472. path2 = ''
  473. kind = 'A'
  474. elif change.type == CHANGE_DELETE:
  475. path1 = change.old.path
  476. path2 = ''
  477. kind = 'D'
  478. elif change.type == CHANGE_MODIFY:
  479. path1 = change.new.path
  480. path2 = ''
  481. kind = 'M'
  482. elif change.type in RENAME_CHANGE_TYPES:
  483. path1 = change.old.path
  484. path2 = change.new.path
  485. if change.type == CHANGE_RENAME:
  486. kind = 'R'
  487. elif change.type == CHANGE_COPY:
  488. kind = 'C'
  489. yield '%-8s%-20s%-20s' % (kind, path1, path2)
  490. def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None,
  491. reverse=False, name_status=False):
  492. """Write commit logs.
  493. :param repo: Path to repository
  494. :param paths: Optional set of specific paths to print entries for
  495. :param outstream: Stream to write log output to
  496. :param reverse: Reverse order in which entries are printed
  497. :param name_status: Print name status
  498. :param max_entries: Optional maximum number of entries to display
  499. """
  500. with open_repo_closing(repo) as r:
  501. walker = r.get_walker(
  502. max_entries=max_entries, paths=paths, reverse=reverse)
  503. for entry in walker:
  504. def decode(x):
  505. return commit_decode(entry.commit, x)
  506. print_commit(entry.commit, decode, outstream)
  507. if name_status:
  508. outstream.writelines(
  509. [l+'\n' for l in print_name_status(entry.changes())])
  510. # TODO(jelmer): better default for encoding?
  511. def show(repo=".", objects=None, outstream=sys.stdout,
  512. default_encoding=DEFAULT_ENCODING):
  513. """Print the changes in a commit.
  514. :param repo: Path to repository
  515. :param objects: Objects to show (defaults to [HEAD])
  516. :param outstream: Stream to write to
  517. :param default_encoding: Default encoding to use if none is set in the
  518. commit
  519. """
  520. if objects is None:
  521. objects = ["HEAD"]
  522. if not isinstance(objects, list):
  523. objects = [objects]
  524. with open_repo_closing(repo) as r:
  525. for objectish in objects:
  526. o = parse_object(r, objectish)
  527. if isinstance(o, Commit):
  528. def decode(x):
  529. return commit_decode(o, x, default_encoding)
  530. else:
  531. def decode(x):
  532. return x.decode(default_encoding)
  533. show_object(r, o, decode, outstream)
  534. def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
  535. """Compares the content and mode of blobs found via two tree objects.
  536. :param repo: Path to repository
  537. :param old_tree: Id of old tree
  538. :param new_tree: Id of new tree
  539. :param outstream: Stream to write to
  540. """
  541. with open_repo_closing(repo) as r:
  542. write_tree_diff(outstream, r.object_store, old_tree, new_tree)
  543. def rev_list(repo, commits, outstream=sys.stdout):
  544. """Lists commit objects in reverse chronological order.
  545. :param repo: Path to repository
  546. :param commits: Commits over which to iterate
  547. :param outstream: Stream to write to
  548. """
  549. with open_repo_closing(repo) as r:
  550. for entry in r.get_walker(include=[r[c].id for c in commits]):
  551. outstream.write(entry.commit.id + b"\n")
  552. def tag(*args, **kwargs):
  553. import warnings
  554. warnings.warn("tag has been deprecated in favour of tag_create.",
  555. DeprecationWarning)
  556. return tag_create(*args, **kwargs)
  557. def tag_create(
  558. repo, tag, author=None, message=None, annotated=False,
  559. objectish="HEAD", tag_time=None, tag_timezone=None):
  560. """Creates a tag in git via dulwich calls:
  561. :param repo: Path to repository
  562. :param tag: tag string
  563. :param author: tag author (optional, if annotated is set)
  564. :param message: tag message (optional)
  565. :param annotated: whether to create an annotated tag
  566. :param objectish: object the tag should point at, defaults to HEAD
  567. :param tag_time: Optional time for annotated tag
  568. :param tag_timezone: Optional timezone for annotated tag
  569. """
  570. with open_repo_closing(repo) as r:
  571. object = parse_object(r, objectish)
  572. if annotated:
  573. # Create the tag object
  574. tag_obj = Tag()
  575. if author is None:
  576. # TODO(jelmer): Don't use repo private method.
  577. author = r._get_user_identity()
  578. tag_obj.tagger = author
  579. tag_obj.message = message
  580. tag_obj.name = tag
  581. tag_obj.object = (type(object), object.id)
  582. if tag_time is None:
  583. tag_time = int(time.time())
  584. tag_obj.tag_time = tag_time
  585. if tag_timezone is None:
  586. # TODO(jelmer) Use current user timezone rather than UTC
  587. tag_timezone = 0
  588. elif isinstance(tag_timezone, str):
  589. tag_timezone = parse_timezone(tag_timezone)
  590. tag_obj.tag_timezone = tag_timezone
  591. r.object_store.add_object(tag_obj)
  592. tag_id = tag_obj.id
  593. else:
  594. tag_id = object.id
  595. r.refs[_make_tag_ref(tag)] = tag_id
  596. def list_tags(*args, **kwargs):
  597. import warnings
  598. warnings.warn("list_tags has been deprecated in favour of tag_list.",
  599. DeprecationWarning)
  600. return tag_list(*args, **kwargs)
  601. def tag_list(repo, outstream=sys.stdout):
  602. """List all tags.
  603. :param repo: Path to repository
  604. :param outstream: Stream to write tags to
  605. """
  606. with open_repo_closing(repo) as r:
  607. tags = sorted(r.refs.as_dict(b"refs/tags"))
  608. return tags
  609. def tag_delete(repo, name):
  610. """Remove a tag.
  611. :param repo: Path to repository
  612. :param name: Name of tag to remove
  613. """
  614. with open_repo_closing(repo) as r:
  615. if isinstance(name, bytes):
  616. names = [name]
  617. elif isinstance(name, list):
  618. names = name
  619. else:
  620. raise TypeError("Unexpected tag name type %r" % name)
  621. for name in names:
  622. del r.refs[_make_tag_ref(name)]
  623. def reset(repo, mode, treeish="HEAD"):
  624. """Reset current HEAD to the specified state.
  625. :param repo: Path to repository
  626. :param mode: Mode ("hard", "soft", "mixed")
  627. :param treeish: Treeish to reset to
  628. """
  629. if mode != "hard":
  630. raise ValueError("hard is the only mode currently supported")
  631. with open_repo_closing(repo) as r:
  632. tree = parse_tree(r, treeish)
  633. r.reset_index(tree.id)
  634. def push(repo, remote_location, refspecs,
  635. outstream=default_bytes_out_stream,
  636. errstream=default_bytes_err_stream, **kwargs):
  637. """Remote push with dulwich via dulwich.client
  638. :param repo: Path to repository
  639. :param remote_location: Location of the remote
  640. :param refspecs: Refs to push to remote
  641. :param outstream: A stream file to write output
  642. :param errstream: A stream file to write errors
  643. """
  644. # Open the repo
  645. with open_repo_closing(repo) as r:
  646. # Get the client and path
  647. client, path = get_transport_and_path(
  648. remote_location, config=r.get_config_stack(), **kwargs)
  649. selected_refs = []
  650. def update_refs(refs):
  651. selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
  652. new_refs = {}
  653. # TODO: Handle selected_refs == {None: None}
  654. for (lh, rh, force) in selected_refs:
  655. if lh is None:
  656. new_refs[rh] = ZERO_SHA
  657. else:
  658. new_refs[rh] = r.refs[lh]
  659. return new_refs
  660. err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING
  661. remote_location_bytes = client.get_url(path).encode(err_encoding)
  662. try:
  663. client.send_pack(
  664. path, update_refs,
  665. generate_pack_data=r.object_store.generate_pack_data,
  666. progress=errstream.write)
  667. errstream.write(
  668. b"Push to " + remote_location_bytes + b" successful.\n")
  669. except (UpdateRefsError, SendPackError) as e:
  670. errstream.write(b"Push to " + remote_location_bytes +
  671. b" failed -> " + e.message.encode(err_encoding) +
  672. b"\n")
  673. def pull(repo, remote_location=None, refspecs=None,
  674. outstream=default_bytes_out_stream,
  675. errstream=default_bytes_err_stream, **kwargs):
  676. """Pull from remote via dulwich.client
  677. :param repo: Path to repository
  678. :param remote_location: Location of the remote
  679. :param refspec: refspecs to fetch
  680. :param outstream: A stream file to write to output
  681. :param errstream: A stream file to write to errors
  682. """
  683. # Open the repo
  684. with open_repo_closing(repo) as r:
  685. if remote_location is None:
  686. # TODO(jelmer): Lookup 'remote' for current branch in config
  687. raise NotImplementedError(
  688. "looking up remote from branch config not supported yet")
  689. if refspecs is None:
  690. refspecs = [b"HEAD"]
  691. selected_refs = []
  692. def determine_wants(remote_refs):
  693. selected_refs.extend(
  694. parse_reftuples(remote_refs, r.refs, refspecs))
  695. return [remote_refs[lh] for (lh, rh, force) in selected_refs]
  696. client, path = get_transport_and_path(
  697. remote_location, config=r.get_config_stack(), **kwargs)
  698. fetch_result = client.fetch(
  699. path, r, progress=errstream.write, determine_wants=determine_wants)
  700. for (lh, rh, force) in selected_refs:
  701. r.refs[rh] = fetch_result.refs[lh]
  702. if selected_refs:
  703. r[b'HEAD'] = fetch_result.refs[selected_refs[0][1]]
  704. # Perform 'git checkout .' - syncs staged changes
  705. tree = r[b"HEAD"].tree
  706. r.reset_index(tree=tree)
  707. def status(repo=".", ignored=False):
  708. """Returns staged, unstaged, and untracked changes relative to the HEAD.
  709. :param repo: Path to repository or repository object
  710. :param ignored: Whether to include ignoed files in `untracked`
  711. :return: GitStatus tuple,
  712. staged - list of staged paths (diff index/HEAD)
  713. unstaged - list of unstaged paths (diff index/working-tree)
  714. untracked - list of untracked, un-ignored & non-.git paths
  715. """
  716. with open_repo_closing(repo) as r:
  717. # 1. Get status of staged
  718. tracked_changes = get_tree_changes(r)
  719. # 2. Get status of unstaged
  720. index = r.open_index()
  721. unstaged_changes = list(get_unstaged_changes(index, r.path))
  722. ignore_manager = IgnoreFilterManager.from_repo(r)
  723. untracked_paths = get_untracked_paths(r.path, r.path, index)
  724. if ignored:
  725. untracked_changes = list(untracked_paths)
  726. else:
  727. untracked_changes = [
  728. p for p in untracked_paths
  729. if not ignore_manager.is_ignored(p)]
  730. return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
  731. def get_untracked_paths(frompath, basepath, index):
  732. """Get untracked paths.
  733. ;param frompath: Path to walk
  734. :param basepath: Path to compare to
  735. :param index: Index to check against
  736. """
  737. # If nothing is specified, add all non-ignored files.
  738. for dirpath, dirnames, filenames in os.walk(frompath):
  739. # Skip .git and below.
  740. if '.git' in dirnames:
  741. dirnames.remove('.git')
  742. if dirpath != basepath:
  743. continue
  744. if '.git' in filenames:
  745. filenames.remove('.git')
  746. if dirpath != basepath:
  747. continue
  748. for filename in filenames:
  749. ap = os.path.join(dirpath, filename)
  750. ip = path_to_tree_path(basepath, ap)
  751. if ip not in index:
  752. yield os.path.relpath(ap, frompath)
  753. def get_tree_changes(repo):
  754. """Return add/delete/modify changes to tree by comparing index to HEAD.
  755. :param repo: repo path or object
  756. :return: dict with lists for each type of change
  757. """
  758. with open_repo_closing(repo) as r:
  759. index = r.open_index()
  760. # Compares the Index to the HEAD & determines changes
  761. # Iterate through the changes and report add/delete/modify
  762. # TODO: call out to dulwich.diff_tree somehow.
  763. tracked_changes = {
  764. 'add': [],
  765. 'delete': [],
  766. 'modify': [],
  767. }
  768. try:
  769. tree_id = r[b'HEAD'].tree
  770. except KeyError:
  771. tree_id = None
  772. for change in index.changes_from_tree(r.object_store, tree_id):
  773. if not change[0][0]:
  774. tracked_changes['add'].append(change[0][1])
  775. elif not change[0][1]:
  776. tracked_changes['delete'].append(change[0][0])
  777. elif change[0][0] == change[0][1]:
  778. tracked_changes['modify'].append(change[0][0])
  779. else:
  780. raise AssertionError('git mv ops not yet supported')
  781. return tracked_changes
  782. def daemon(path=".", address=None, port=None):
  783. """Run a daemon serving Git requests over TCP/IP.
  784. :param path: Path to the directory to serve.
  785. :param address: Optional address to listen on (defaults to ::)
  786. :param port: Optional port to listen on (defaults to TCP_GIT_PORT)
  787. """
  788. # TODO(jelmer): Support git-daemon-export-ok and --export-all.
  789. backend = FileSystemBackend(path)
  790. server = TCPGitServer(backend, address, port)
  791. server.serve_forever()
  792. def web_daemon(path=".", address=None, port=None):
  793. """Run a daemon serving Git requests over HTTP.
  794. :param path: Path to the directory to serve
  795. :param address: Optional address to listen on (defaults to ::)
  796. :param port: Optional port to listen on (defaults to 80)
  797. """
  798. from dulwich.web import (
  799. make_wsgi_chain,
  800. make_server,
  801. WSGIRequestHandlerLogger,
  802. WSGIServerLogger)
  803. backend = FileSystemBackend(path)
  804. app = make_wsgi_chain(backend)
  805. server = make_server(address, port, app,
  806. handler_class=WSGIRequestHandlerLogger,
  807. server_class=WSGIServerLogger)
  808. server.serve_forever()
  809. def upload_pack(path=".", inf=None, outf=None):
  810. """Upload a pack file after negotiating its contents using smart protocol.
  811. :param path: Path to the repository
  812. :param inf: Input stream to communicate with client
  813. :param outf: Output stream to communicate with client
  814. """
  815. if outf is None:
  816. outf = getattr(sys.stdout, 'buffer', sys.stdout)
  817. if inf is None:
  818. inf = getattr(sys.stdin, 'buffer', sys.stdin)
  819. path = os.path.expanduser(path)
  820. backend = FileSystemBackend(path)
  821. def send_fn(data):
  822. outf.write(data)
  823. outf.flush()
  824. proto = Protocol(inf.read, send_fn)
  825. handler = UploadPackHandler(backend, [path], proto)
  826. # FIXME: Catch exceptions and write a single-line summary to outf.
  827. handler.handle()
  828. return 0
  829. def receive_pack(path=".", inf=None, outf=None):
  830. """Receive a pack file after negotiating its contents using smart protocol.
  831. :param path: Path to the repository
  832. :param inf: Input stream to communicate with client
  833. :param outf: Output stream to communicate with client
  834. """
  835. if outf is None:
  836. outf = getattr(sys.stdout, 'buffer', sys.stdout)
  837. if inf is None:
  838. inf = getattr(sys.stdin, 'buffer', sys.stdin)
  839. path = os.path.expanduser(path)
  840. backend = FileSystemBackend(path)
  841. def send_fn(data):
  842. outf.write(data)
  843. outf.flush()
  844. proto = Protocol(inf.read, send_fn)
  845. handler = ReceivePackHandler(backend, [path], proto)
  846. # FIXME: Catch exceptions and write a single-line summary to outf.
  847. handler.handle()
  848. return 0
  849. def _make_branch_ref(name):
  850. if getattr(name, 'encode', None):
  851. name = name.encode(DEFAULT_ENCODING)
  852. return b"refs/heads/" + name
  853. def _make_tag_ref(name):
  854. if getattr(name, 'encode', None):
  855. name = name.encode(DEFAULT_ENCODING)
  856. return b"refs/tags/" + name
  857. def branch_delete(repo, name):
  858. """Delete a branch.
  859. :param repo: Path to the repository
  860. :param name: Name of the branch
  861. """
  862. with open_repo_closing(repo) as r:
  863. if isinstance(name, list):
  864. names = name
  865. else:
  866. names = [name]
  867. for name in names:
  868. del r.refs[_make_branch_ref(name)]
  869. def branch_create(repo, name, objectish=None, force=False):
  870. """Create a branch.
  871. :param repo: Path to the repository
  872. :param name: Name of the new branch
  873. :param objectish: Target object to point new branch at (defaults to HEAD)
  874. :param force: Force creation of branch, even if it already exists
  875. """
  876. with open_repo_closing(repo) as r:
  877. if objectish is None:
  878. objectish = "HEAD"
  879. object = parse_object(r, objectish)
  880. refname = _make_branch_ref(name)
  881. ref_message = b"branch: Created from " + objectish.encode('utf-8')
  882. if force:
  883. r.refs.set_if_equals(refname, None, object.id, message=ref_message)
  884. else:
  885. if not r.refs.add_if_new(refname, object.id, message=ref_message):
  886. raise KeyError("Branch with name %s already exists." % name)
  887. def branch_list(repo):
  888. """List all branches.
  889. :param repo: Path to the repository
  890. """
  891. with open_repo_closing(repo) as r:
  892. return r.refs.keys(base=b"refs/heads/")
  893. def fetch(repo, remote_location, outstream=sys.stdout,
  894. errstream=default_bytes_err_stream, **kwargs):
  895. """Fetch objects from a remote server.
  896. :param repo: Path to the repository
  897. :param remote_location: String identifying a remote server
  898. :param outstream: Output stream (defaults to stdout)
  899. :param errstream: Error stream (defaults to stderr)
  900. :return: Dictionary with refs on the remote
  901. """
  902. with open_repo_closing(repo) as r:
  903. client, path = get_transport_and_path(
  904. remote_location, config=r.get_config_stack(), **kwargs)
  905. remote_refs = client.fetch(path, r, progress=errstream.write)
  906. return remote_refs
  907. def ls_remote(remote, config=None, **kwargs):
  908. """List the refs in a remote.
  909. :param remote: Remote repository location
  910. :param config: Configuration to use
  911. :return: Dictionary with remote refs
  912. """
  913. if config is None:
  914. config = StackedConfig.default()
  915. client, host_path = get_transport_and_path(remote, config=config, **kwargs)
  916. return client.get_refs(host_path)
  917. def repack(repo):
  918. """Repack loose files in a repository.
  919. Currently this only packs loose objects.
  920. :param repo: Path to the repository
  921. """
  922. with open_repo_closing(repo) as r:
  923. r.object_store.pack_loose_objects()
  924. def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
  925. """Pack objects into a file.
  926. :param repo: Path to the repository
  927. :param object_ids: List of object ids to write
  928. :param packf: File-like object to write to
  929. :param idxf: File-like object to write to (can be None)
  930. """
  931. with open_repo_closing(repo) as r:
  932. entries, data_sum = write_pack_objects(
  933. packf,
  934. r.object_store.iter_shas((oid, None) for oid in object_ids),
  935. delta_window_size=delta_window_size)
  936. if idxf is not None:
  937. entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()])
  938. write_pack_index(idxf, entries, data_sum)
  939. def ls_tree(repo, treeish=b"HEAD", outstream=sys.stdout, recursive=False,
  940. name_only=False):
  941. """List contents of a tree.
  942. :param repo: Path to the repository
  943. :param tree_ish: Tree id to list
  944. :param outstream: Output stream (defaults to stdout)
  945. :param recursive: Whether to recursively list files
  946. :param name_only: Only print item name
  947. """
  948. def list_tree(store, treeid, base):
  949. for (name, mode, sha) in store[treeid].iteritems():
  950. if base:
  951. name = posixpath.join(base, name)
  952. if name_only:
  953. outstream.write(name + b"\n")
  954. else:
  955. outstream.write(pretty_format_tree_entry(name, mode, sha))
  956. if stat.S_ISDIR(mode):
  957. list_tree(store, sha, name)
  958. with open_repo_closing(repo) as r:
  959. tree = parse_tree(r, treeish)
  960. list_tree(r.object_store, tree.id, "")
  961. def remote_add(repo, name, url):
  962. """Add a remote.
  963. :param repo: Path to the repository
  964. :param name: Remote name
  965. :param url: Remote URL
  966. """
  967. if not isinstance(name, bytes):
  968. name = name.encode(DEFAULT_ENCODING)
  969. if not isinstance(url, bytes):
  970. url = url.encode(DEFAULT_ENCODING)
  971. with open_repo_closing(repo) as r:
  972. c = r.get_config()
  973. section = (b'remote', name)
  974. if c.has_section(section):
  975. raise RemoteExists(section)
  976. c.set(section, b"url", url)
  977. c.write_to_path()
  978. def check_ignore(repo, paths, no_index=False):
  979. """Debug gitignore files.
  980. :param repo: Path to the repository
  981. :param paths: List of paths to check for
  982. :param no_index: Don't check index
  983. :return: List of ignored files
  984. """
  985. with open_repo_closing(repo) as r:
  986. index = r.open_index()
  987. ignore_manager = IgnoreFilterManager.from_repo(r)
  988. for path in paths:
  989. if os.path.isabs(path):
  990. path = os.path.relpath(path, r.path)
  991. if not no_index and path_to_tree_path(r.path, path) in index:
  992. continue
  993. if ignore_manager.is_ignored(path):
  994. yield path
  995. def update_head(repo, target, detached=False, new_branch=None):
  996. """Update HEAD to point at a new branch/commit.
  997. Note that this does not actually update the working tree.
  998. :param repo: Path to the repository
  999. :param detach: Create a detached head
  1000. :param target: Branch or committish to switch to
  1001. :param new_branch: New branch to create
  1002. """
  1003. with open_repo_closing(repo) as r:
  1004. if new_branch is not None:
  1005. to_set = _make_branch_ref(new_branch)
  1006. else:
  1007. to_set = b"HEAD"
  1008. if detached:
  1009. # TODO(jelmer): Provide some way so that the actual ref gets
  1010. # updated rather than what it points to, so the delete isn't
  1011. # necessary.
  1012. del r.refs[to_set]
  1013. r.refs[to_set] = parse_commit(r, target).id
  1014. else:
  1015. r.refs.set_symbolic_ref(to_set, parse_ref(r, target))
  1016. if new_branch is not None:
  1017. r.refs.set_symbolic_ref(b"HEAD", to_set)