| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -87,22 +87,21 @@ Refs | |||
| 87 | 87 | ----- | |
| 88 | 88 | * When adjusting the reference of a symbolic reference, the ref log might need | |
| 89 | 89 | adjustments as well. This is not critical, but would make things totally 'right' | |
| 90 | - * Reference Objects should be able to set the commit they are pointing to, making | ||
| 91 | - the commit property read-write. Tags are a special case of this and would need | ||
| 92 | - to be handled as well ! | ||
| 93 | - * Ability to create new heads and tags in the Repository ( but using the respective | ||
| 94 | - Reference Type ), i.e. Head.create(repo, name, commit = 'HEAD') or | ||
| 95 | - TagReference.create(repo, name | ||
| 96 | - * Ability to rename references and tags | ||
| 97 | - * Ability to remove references and tags | ||
| 98 | - * Ability to checkout a reference - | ||
| 99 | - * Check whether we are the active reference HEAD.commit == self.commit | ||
| 90 | + - same with adjusting references directly | ||
| 91 | + !! - Could simply rewrite it using git-update-ref which works nicely for symbolic | ||
| 92 | + and for normal refs !! | ||
| 93 | + * Check whether we are the active reference HEAD.reference == this_ref | ||
| 94 | + - NO: The reference dosnt need to know - in fact it does not know about the | ||
| 95 | + main HEAD, so it may not use it. This is to be done in client code only. | ||
| 96 | + Remove me | ||
| 100 | 97 | ||
| 101 | 98 | Remote | |
| 102 | 99 | ------ | |
| 103 | 100 | * 'push' method needs a test, a true test repository is required though, a fork | |
| 104 | 101 | of a fork would do :)! | |
| 105 | 102 | * Fetch should return heads that where updated, pull as well. | |
| 103 | + * Creation and deletion methods for references should be part of the interface, allowing | ||
| 104 | + repo.create_head(...) instaed of Head.create(repo, ...). Its a convenience thing, clearly | ||
| 106 | 105 | ||
| 107 | 106 | Repo | |
| 108 | 107 | ---- | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,7 +19,7 @@ class Reference(LazyMixin, Iterable): | |||
| 19 | 19 | _common_path_default = "refs" | |
| 20 | 20 | _id_attribute_ = "name" | |
| 21 | 21 | ||
| 22 | - def __init__(self, repo, path, object = None): | ||
| 22 | + def __init__(self, repo, path): | ||
| 23 | 23 | """ | |
| 24 | 24 | Initialize this instance | |
| 25 | 25 | ``repo`` | |
@@ -29,16 +29,12 @@ def __init__(self, repo, path, object = None): | |||
| 29 | 29 | Path relative to the .git/ directory pointing to the ref in question, i.e. | |
| 30 | 30 | refs/heads/master | |
| 31 | 31 | ||
| 32 | - ``object`` | ||
| 33 | - Object instance, will be retrieved on demand if None | ||
| 34 | 32 | """ | |
| 35 | 33 | if not path.startswith(self._common_path_default): | |
| 36 | 34 | raise ValueError("Cannot instantiate %s Reference from path %s" % ( self.__class__.__name__, path )) | |
| 37 | 35 | ||
| 38 | 36 | self.repo = repo | |
| 39 | 37 | self.path = path | |
| 40 | - if object is not None: | ||
| 41 | - self.object = object | ||
| 42 | 38 | ||
| 43 | 39 | def __str__(self): | |
| 44 | 40 | return self.name | |
@@ -69,8 +65,7 @@ def name(self): | |||
| 69 | 65 | ||
| 70 | 66 | return '/'.join(tokens[2:]) | |
| 71 | 67 | ||
| 72 | - @property | ||
| 73 | - def object(self): | ||
| 68 | + def _get_object(self): | ||
| 74 | 69 | """ | |
| 75 | 70 | Returns | |
| 76 | 71 | The object our ref currently refers to. Refs can be cached, they will | |
@@ -80,17 +75,54 @@ def object(self): | |||
| 80 | 75 | # Our path will be resolved to the hexsha which will be used accordingly | |
| 81 | 76 | return Object.new(self.repo, self.path) | |
| 82 | 77 | ||
| 83 | - @property | ||
| 84 | - def commit(self): | ||
| 78 | + def _set_object(self, ref, type=None): | ||
| 79 | + """ | ||
| 80 | + Set our reference to point to the given ref. It will be converted | ||
| 81 | + to a specific hexsha. | ||
| 82 | + | ||
| 83 | + ``type`` | ||
| 84 | + If not None, string type of that the object must have, other we raise | ||
| 85 | + a type error. Only used internally | ||
| 86 | + | ||
| 87 | + Returns | ||
| 88 | + Object we have set. This is used internally only to reduce the amount | ||
| 89 | + of calls to the git command | ||
| 90 | + """ | ||
| 91 | + obj = Object.new(self.repo, ref) | ||
| 92 | + if type is not None and obj.type != type: | ||
| 93 | + raise TypeError("Reference %r cannot point to object of type %r" % (self,obj.type)) | ||
| 94 | + | ||
| 95 | + full_ref_path = os.path.join(self.repo.path, self.path) | ||
| 96 | + fp = open(full_ref_path, "w") | ||
| 97 | + try: | ||
| 98 | + fp.write(str(obj)) | ||
| 99 | + finally: | ||
| 100 | + fp.close() | ||
| 101 | + return obj | ||
| 102 | + | ||
| 103 | + object = property(_get_object, _set_object, doc="Return the object our ref currently refers to") | ||
| 104 | + | ||
| 105 | + def _set_commit(self, commit): | ||
| 106 | + """ | ||
| 107 | + Set ourselves to point to the given commit. | ||
| 108 | + | ||
| 109 | + Raise | ||
| 110 | + ValueError if commit does not actually point to a commit | ||
| 111 | + """ | ||
| 112 | + self._set_object(commit, type="commit") | ||
| 113 | + | ||
| 114 | + def _get_commit(self): | ||
| 85 | 115 | """ | |
| 86 | 116 | Returns | |
| 87 | - Commit object the head points to | ||
| 117 | + Commit object the reference points to | ||
| 88 | 118 | """ | |
| 89 | 119 | commit = self.object | |
| 90 | 120 | if commit.type != "commit": | |
| 91 | 121 | raise TypeError("Object of reference %s did not point to a commit" % self) | |
| 92 | 122 | return commit | |
| 93 | 123 | ||
| 124 | + commit = property(_get_commit, _set_commit, doc="Return Commit object the reference points to") | ||
| 125 | + | ||
| 94 | 126 | @classmethod | |
| 95 | 127 | def iter_items(cls, repo, common_path = None, **kwargs): | |
| 96 | 128 | """ | |
@@ -182,6 +214,7 @@ def _from_string(cls, repo, line): | |||
| 182 | 214 | # obj.size = object_size | |
| 183 | 215 | # return cls(repo, full_path, obj) | |
| 184 | 216 | ||
| 217 | + | ||
| 185 | 218 | ||
| 186 | 219 | class SymbolicReference(object): | |
| 187 | 220 | """ | |
@@ -247,7 +280,7 @@ def _get_reference(self): | |||
| 247 | 280 | try: | |
| 248 | 281 | tokens = fp.readline().rstrip().split(' ') | |
| 249 | 282 | if tokens[0] != 'ref:': | |
| 250 | - raise TypeError("%s is a detached symbolic reference as it points to %r" % tokens[0]) | ||
| 283 | + raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, tokens[0])) | ||
| 251 | 284 | return Reference.from_path(self.repo, tokens[1]) | |
| 252 | 285 | finally: | |
| 253 | 286 | fp.close() | |
@@ -384,12 +417,92 @@ class Head(Reference): | |||
| 384 | 417 | """ | |
| 385 | 418 | _common_path_default = "refs/heads" | |
| 386 | 419 | ||
| 420 | + @classmethod | ||
| 421 | + def create(cls, repo, path, commit='HEAD', force=False, **kwargs ): | ||
| 422 | + """ | ||
| 423 | + Create a new head. | ||
| 424 | + ``repo`` | ||
| 425 | + Repository to create the head in | ||
| 426 | + | ||
| 427 | + ``path`` | ||
| 428 | + The name or path of the head, i.e. 'new_branch' or | ||
| 429 | + feature/feature1. The prefix refs/heads is implied. | ||
| 430 | + | ||
| 431 | + ``commit`` | ||
| 432 | + Commit to which the new head should point, defaults to the | ||
| 433 | + current HEAD | ||
| 434 | + | ||
| 435 | + ``force`` | ||
| 436 | + if True, force creation even if branch with that name already exists. | ||
| 437 | + | ||
| 438 | + ``**kwargs`` | ||
| 439 | + Additional keyword arguments to be passed to git-branch, i.e. | ||
| 440 | + track, no-track, l | ||
| 441 | + | ||
| 442 | + Returns | ||
| 443 | + Newly created Head | ||
| 444 | + | ||
| 445 | + Note | ||
| 446 | + This does not alter the current HEAD, index or Working Tree | ||
| 447 | + """ | ||
| 448 | + if cls is not Head: | ||
| 449 | + raise TypeError("Only Heads can be created explicitly, not objects of type %s" % cls.__name__) | ||
| 450 | + | ||
| 451 | + args = ( path, commit ) | ||
| 452 | + if force: | ||
| 453 | + kwargs['f'] = True | ||
| 454 | + | ||
| 455 | + repo.git.branch(*args, **kwargs) | ||
| 456 | + return cls(repo, "%s/%s" % ( cls._common_path_default, path)) | ||
| 457 | + | ||
| 458 | + | ||
| 459 | + @classmethod | ||
| 460 | + def delete(cls, repo, *heads, **kwargs): | ||
| 461 | + """ | ||
| 462 | + Delete the given heads | ||
| 463 | + | ||
| 464 | + ``force`` | ||
| 465 | + If True, the heads will be deleted even if they are not yet merged into | ||
| 466 | + the main development stream. | ||
| 467 | + Default False | ||
| 468 | + """ | ||
| 469 | + force = kwargs.get("force", False) | ||
| 470 | + flag = "-d" | ||
| 471 | + if force: | ||
| 472 | + flag = "-D" | ||
| 473 | + repo.git.branch(flag, *heads) | ||
| 474 | + | ||
| 475 | + | ||
| 476 | + def rename(self, new_path, force=False): | ||
| 477 | + """ | ||
| 478 | + Rename self to a new path | ||
| 479 | + | ||
| 480 | + ``new_path`` | ||
| 481 | + Either a simple name or a path, i.e. new_name or features/new_name. | ||
| 482 | + The prefix refs/heads is implied | ||
| 483 | + | ||
| 484 | + ``force`` | ||
| 485 | + If True, the rename will succeed even if a head with the target name | ||
| 486 | + already exists. | ||
| 487 | + | ||
| 488 | + Returns | ||
| 489 | + self | ||
| 490 | + """ | ||
| 491 | + flag = "-m" | ||
| 492 | + if force: | ||
| 493 | + flag = "-M" | ||
| 494 | + | ||
| 495 | + self.repo.git.branch(flag, self, new_path) | ||
| 496 | + self.path = "%s/%s" % (self._common_path_default, new_path) | ||
| 497 | + return self | ||
| 498 | + | ||
| 499 | + | ||
| 387 | 500 | ||
| 388 | 501 | class TagReference(Reference): | |
| 389 | 502 | """ | |
| 390 | 503 | Class representing a lightweight tag reference which either points to a commit | |
| 391 | - or to a tag object. In the latter case additional information, like the signature | ||
| 392 | - or the tag-creator, is available. | ||
| 504 | + ,a tag object or any other object. In the latter case additional information, | ||
| 505 | + like the signature or the tag-creator, is available. | ||
| 393 | 506 | ||
| 394 | 507 | This tag object will always point to a commit object, but may carray additional | |
| 395 | 508 | information in a tag object:: | |
@@ -427,6 +540,52 @@ def tag(self): | |||
| 427 | 540 | if self.object.type == "tag": | |
| 428 | 541 | return self.object | |
| 429 | 542 | return None | |
| 543 | + | ||
| 544 | + @classmethod | ||
| 545 | + def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs): | ||
| 546 | + """ | ||
| 547 | + Create a new tag object. | ||
| 548 | + | ||
| 549 | + ``path`` | ||
| 550 | + The name of the tag, i.e. 1.0 or releases/1.0. | ||
| 551 | + The prefix refs/tags is implied | ||
| 552 | + | ||
| 553 | + ``ref`` | ||
| 554 | + A reference to the object you want to tag. It can be a commit, tree or | ||
| 555 | + blob. | ||
| 556 | + | ||
| 557 | + ``message`` | ||
| 558 | + If not None, the message will be used in your tag object. This will also | ||
| 559 | + create an additional tag object that allows to obtain that information, i.e.:: | ||
| 560 | + tagref.tag.message | ||
| 561 | + | ||
| 562 | + ``force`` | ||
| 563 | + If True, to force creation of a tag even though that tag already exists. | ||
| 564 | + | ||
| 565 | + ``**kwargs`` | ||
| 566 | + Additional keyword arguments to be passed to git-tag | ||
| 567 | + | ||
| 568 | + Returns | ||
| 569 | + A new TagReference | ||
| 570 | + """ | ||
| 571 | + args = ( path, ref ) | ||
| 572 | + if message: | ||
| 573 | + kwargs['m'] = message | ||
| 574 | + if force: | ||
| 575 | + kwargs['f'] = True | ||
| 576 | + | ||
| 577 | + repo.git.tag(*args, **kwargs) | ||
| 578 | + return TagReference(repo, "%s/%s" % (cls._common_path_default, path)) | ||
| 579 | + | ||
| 580 | + @classmethod | ||
| 581 | + def delete(cls, repo, *tags): | ||
| 582 | + """ | ||
| 583 | + Delete the given existing tag or tags | ||
| 584 | + """ | ||
| 585 | + repo.git.tag("-d", *tags) | ||
| 586 | + | ||
| 587 | + | ||
| 588 | + | ||
| 430 | 589 | ||
| 431 | 590 | ||
| 432 | 591 | # provide an alias | |
@@ -460,3 +619,14 @@ def remote_branch(self): | |||
| 460 | 619 | """ | |
| 461 | 620 | tokens = self.path.split('/') | |
| 462 | 621 | return '/'.join(tokens[3:]) | |
| 622 | + | ||
| 623 | + @classmethod | ||
| 624 | + def delete(cls, repo, *remotes, **kwargs): | ||
| 625 | + """ | ||
| 626 | + Delete the given remote references. | ||
| 627 | + | ||
| 628 | + Note | ||
| 629 | + kwargs are given for compatability with the base class method as we | ||
| 630 | + should not narrow the signature. | ||
| 631 | + """ | ||
| 632 | + repo.git.branch("-d", "-r", *remotes) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -108,3 +108,93 @@ def test_head_reset(self, rw_repo): | |||
| 108 | 108 | ||
| 109 | 109 | # type check | |
| 110 | 110 | self.failUnlessRaises(ValueError, setattr, cur_head, "reference", "that") | |
| 111 | + | ||
| 112 | + # head handling | ||
| 113 | + commit = 'HEAD' | ||
| 114 | + prev_head_commit = cur_head.commit | ||
| 115 | + for count, new_name in enumerate(("my_new_head", "feature/feature1")): | ||
| 116 | + actual_commit = commit+"^"*count | ||
| 117 | + new_head = Head.create(rw_repo, new_name, actual_commit) | ||
| 118 | + assert cur_head.commit == prev_head_commit | ||
| 119 | + assert isinstance(new_head, Head) | ||
| 120 | + # already exists | ||
| 121 | + self.failUnlessRaises(GitCommandError, Head.create, rw_repo, new_name) | ||
| 122 | + | ||
| 123 | + # force it | ||
| 124 | + new_head = Head.create(rw_repo, new_name, actual_commit, force=True) | ||
| 125 | + old_path = new_head.path | ||
| 126 | + old_name = new_head.name | ||
| 127 | + | ||
| 128 | + assert new_head.rename("hello").name == "hello" | ||
| 129 | + assert new_head.rename("hello/world").name == "hello/world" | ||
| 130 | + assert new_head.rename(old_name).name == old_name and new_head.path == old_path | ||
| 131 | + | ||
| 132 | + # rename with force | ||
| 133 | + tmp_head = Head.create(rw_repo, "tmphead") | ||
| 134 | + self.failUnlessRaises(GitCommandError, tmp_head.rename, new_head) | ||
| 135 | + tmp_head.rename(new_head, force=True) | ||
| 136 | + assert tmp_head == new_head and tmp_head.object == new_head.object | ||
| 137 | + | ||
| 138 | + Head.delete(rw_repo, tmp_head) | ||
| 139 | + heads = rw_repo.heads | ||
| 140 | + assert tmp_head not in heads and new_head not in heads | ||
| 141 | + # force on deletion testing would be missing here, code looks okay though ;) | ||
| 142 | + # END for each new head name | ||
| 143 | + self.failUnlessRaises(TypeError, RemoteReference.create, rw_repo, "some_name") | ||
| 144 | + | ||
| 145 | + # tag ref | ||
| 146 | + tag_name = "1.0.2" | ||
| 147 | + light_tag = TagReference.create(rw_repo, tag_name) | ||
| 148 | + self.failUnlessRaises(GitCommandError, TagReference.create, rw_repo, tag_name) | ||
| 149 | + light_tag = TagReference.create(rw_repo, tag_name, "HEAD~1", force = True) | ||
| 150 | + assert isinstance(light_tag, TagReference) | ||
| 151 | + assert light_tag.name == tag_name | ||
| 152 | + assert light_tag.commit == cur_head.commit.parents[0] | ||
| 153 | + assert light_tag.tag is None | ||
| 154 | + | ||
| 155 | + # tag with tag object | ||
| 156 | + other_tag_name = "releases/1.0.2RC" | ||
| 157 | + msg = "my mighty tag\nsecond line" | ||
| 158 | + obj_tag = TagReference.create(rw_repo, other_tag_name, message=msg) | ||
| 159 | + assert isinstance(obj_tag, TagReference) | ||
| 160 | + assert obj_tag.name == other_tag_name | ||
| 161 | + assert obj_tag.commit == cur_head.commit | ||
| 162 | + assert obj_tag.tag is not None | ||
| 163 | + | ||
| 164 | + TagReference.delete(rw_repo, light_tag, obj_tag) | ||
| 165 | + tags = rw_repo.tags | ||
| 166 | + assert light_tag not in tags and obj_tag not in tags | ||
| 167 | + | ||
| 168 | + # remote deletion | ||
| 169 | + remote_refs_so_far = 0 | ||
| 170 | + remotes = rw_repo.remotes | ||
| 171 | + assert remotes | ||
| 172 | + for remote in remotes: | ||
| 173 | + refs = remote.refs | ||
| 174 | + RemoteReference.delete(rw_repo, *refs) | ||
| 175 | + remote_refs_so_far += len(refs) | ||
| 176 | + # END for each ref to delete | ||
| 177 | + assert remote_refs_so_far | ||
| 178 | + | ||
| 179 | + for remote in remotes: | ||
| 180 | + # remotes without references throw | ||
| 181 | + self.failUnlessRaises(AssertionError, getattr, remote, 'refs') | ||
| 182 | + # END for each remote | ||
| 183 | + | ||
| 184 | + # change where the active head points to | ||
| 185 | + if cur_head.is_detached: | ||
| 186 | + cur_head.reference = rw_repo.heads[0] | ||
| 187 | + | ||
| 188 | + head = cur_head.reference | ||
| 189 | + old_commit = head.commit | ||
| 190 | + head.commit = old_commit.parents[0] | ||
| 191 | + assert head.commit == old_commit.parents[0] | ||
| 192 | + assert head.commit == cur_head.commit | ||
| 193 | + head.commit = old_commit | ||
| 194 | + | ||
| 195 | + # setting a non-commit as commit fails, but succeeds as object | ||
| 196 | + head_tree = head.commit.tree | ||
| 197 | + self.failUnlessRaises(TypeError, setattr, head, 'commit', head_tree) | ||
| 198 | + assert head.commit == old_commit # and the ref did not change | ||
| 199 | + head.object = head_tree | ||
| 200 | + assert head.object == head_tree | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments