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