mirror of https://github.com/morpheus65535/bazarr
1204 lines
53 KiB
Python
1204 lines
53 KiB
Python
# need a dict to set bloody .name field
|
|
from io import BytesIO
|
|
import logging
|
|
import os
|
|
import stat
|
|
from unittest import SkipTest
|
|
import uuid
|
|
|
|
import git
|
|
from git.cmd import Git
|
|
from git.compat import (
|
|
string_types,
|
|
defenc,
|
|
is_win,
|
|
)
|
|
from git.config import (
|
|
SectionConstraint,
|
|
GitConfigParser,
|
|
cp
|
|
)
|
|
from git.exc import (
|
|
InvalidGitRepositoryError,
|
|
NoSuchPathError,
|
|
RepositoryDirtyError
|
|
)
|
|
from git.objects.base import IndexObject, Object
|
|
from git.objects.util import Traversable
|
|
from git.util import (
|
|
Iterable,
|
|
join_path_native,
|
|
to_native_path_linux,
|
|
RemoteProgress,
|
|
rmtree,
|
|
unbare_repo
|
|
)
|
|
from git.util import HIDE_WINDOWS_KNOWN_ERRORS
|
|
|
|
import os.path as osp
|
|
|
|
from .util import (
|
|
mkhead,
|
|
sm_name,
|
|
sm_section,
|
|
SubmoduleConfigParser,
|
|
find_first_remote_branch
|
|
)
|
|
|
|
|
|
__all__ = ["Submodule", "UpdateProgress"]
|
|
|
|
|
|
log = logging.getLogger('git.objects.submodule.base')
|
|
log.addHandler(logging.NullHandler())
|
|
|
|
|
|
class UpdateProgress(RemoteProgress):
|
|
|
|
"""Class providing detailed progress information to the caller who should
|
|
derive from it and implement the ``update(...)`` message"""
|
|
CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
|
|
_num_op_codes = RemoteProgress._num_op_codes + 3
|
|
|
|
__slots__ = ()
|
|
|
|
|
|
BEGIN = UpdateProgress.BEGIN
|
|
END = UpdateProgress.END
|
|
CLONE = UpdateProgress.CLONE
|
|
FETCH = UpdateProgress.FETCH
|
|
UPDWKTREE = UpdateProgress.UPDWKTREE
|
|
|
|
|
|
# IndexObject comes via util module, its a 'hacky' fix thanks to pythons import
|
|
# mechanism which cause plenty of trouble of the only reason for packages and
|
|
# modules is refactoring - subpackages shouldn't depend on parent packages
|
|
class Submodule(IndexObject, Iterable, Traversable):
|
|
|
|
"""Implements access to a git submodule. They are special in that their sha
|
|
represents a commit in the submodule's repository which is to be checked out
|
|
at the path of this instance.
|
|
The submodule type does not have a string type associated with it, as it exists
|
|
solely as a marker in the tree and index.
|
|
|
|
All methods work in bare and non-bare repositories."""
|
|
|
|
_id_attribute_ = "name"
|
|
k_modules_file = '.gitmodules'
|
|
k_head_option = 'branch'
|
|
k_head_default = 'master'
|
|
k_default_mode = stat.S_IFDIR | stat.S_IFLNK # submodules are directories with link-status
|
|
|
|
# this is a bogus type for base class compatibility
|
|
type = 'submodule'
|
|
|
|
__slots__ = ('_parent_commit', '_url', '_branch_path', '_name', '__weakref__')
|
|
_cache_attrs = ('path', '_url', '_branch_path')
|
|
|
|
def __init__(self, repo, binsha, mode=None, path=None, name=None, parent_commit=None, url=None, branch_path=None):
|
|
"""Initialize this instance with its attributes. We only document the ones
|
|
that differ from ``IndexObject``
|
|
|
|
:param repo: Our parent repository
|
|
:param binsha: binary sha referring to a commit in the remote repository, see url parameter
|
|
:param parent_commit: see set_parent_commit()
|
|
:param url: The url to the remote repository which is the submodule
|
|
:param branch_path: full (relative) path to ref to checkout when cloning the remote repository"""
|
|
super(Submodule, self).__init__(repo, binsha, mode, path)
|
|
self.size = 0
|
|
self._parent_commit = parent_commit
|
|
if url is not None:
|
|
self._url = url
|
|
if branch_path is not None:
|
|
assert isinstance(branch_path, string_types)
|
|
self._branch_path = branch_path
|
|
if name is not None:
|
|
self._name = name
|
|
|
|
def _set_cache_(self, attr):
|
|
if attr in ('path', '_url', '_branch_path'):
|
|
reader = self.config_reader()
|
|
# default submodule values
|
|
try:
|
|
self.path = reader.get('path')
|
|
except cp.NoSectionError:
|
|
raise ValueError("This submodule instance does not exist anymore in '%s' file"
|
|
% osp.join(self.repo.working_tree_dir, '.gitmodules'))
|
|
# end
|
|
self._url = reader.get('url')
|
|
# git-python extension values - optional
|
|
self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
|
|
elif attr == '_name':
|
|
raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
|
|
else:
|
|
super(Submodule, self)._set_cache_(attr)
|
|
# END handle attribute name
|
|
|
|
def _get_intermediate_items(self, item):
|
|
""":return: all the submodules of our module repository"""
|
|
try:
|
|
return type(self).list_items(item.module())
|
|
except InvalidGitRepositoryError:
|
|
return []
|
|
# END handle intermediate items
|
|
|
|
@classmethod
|
|
def _need_gitfile_submodules(cls, git):
|
|
return git.version_info[:3] >= (1, 7, 5)
|
|
|
|
def __eq__(self, other):
|
|
"""Compare with another submodule"""
|
|
# we may only compare by name as this should be the ID they are hashed with
|
|
# Otherwise this type wouldn't be hashable
|
|
# return self.path == other.path and self.url == other.url and super(Submodule, self).__eq__(other)
|
|
return self._name == other._name
|
|
|
|
def __ne__(self, other):
|
|
"""Compare with another submodule for inequality"""
|
|
return not (self == other)
|
|
|
|
def __hash__(self):
|
|
"""Hash this instance using its logical id, not the sha"""
|
|
return hash(self._name)
|
|
|
|
def __str__(self):
|
|
return self._name
|
|
|
|
def __repr__(self):
|
|
return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)"\
|
|
% (type(self).__name__, self._name, self.path, self.url, self.branch_path)
|
|
|
|
@classmethod
|
|
def _config_parser(cls, repo, parent_commit, read_only):
|
|
""":return: Config Parser constrained to our submodule in read or write mode
|
|
:raise IOError: If the .gitmodules file cannot be found, either locally or in the repository
|
|
at the given parent commit. Otherwise the exception would be delayed until the first
|
|
access of the config parser"""
|
|
parent_matches_head = True
|
|
if parent_commit is not None:
|
|
try:
|
|
parent_matches_head = repo.head.commit == parent_commit
|
|
except ValueError:
|
|
# We are most likely in an empty repository, so the HEAD doesn't point to a valid ref
|
|
pass
|
|
# end handle parent_commit
|
|
|
|
if not repo.bare and parent_matches_head:
|
|
fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file)
|
|
else:
|
|
assert parent_commit is not None, "need valid parent_commit in bare repositories"
|
|
try:
|
|
fp_module = cls._sio_modules(parent_commit)
|
|
except KeyError:
|
|
raise IOError("Could not find %s file in the tree of parent commit %s" %
|
|
(cls.k_modules_file, parent_commit))
|
|
# END handle exceptions
|
|
# END handle non-bare working tree
|
|
|
|
if not read_only and (repo.bare or not parent_matches_head):
|
|
raise ValueError("Cannot write blobs of 'historical' submodule configurations")
|
|
# END handle writes of historical submodules
|
|
|
|
return SubmoduleConfigParser(fp_module, read_only=read_only)
|
|
|
|
def _clear_cache(self):
|
|
# clear the possibly changed values
|
|
for name in self._cache_attrs:
|
|
try:
|
|
delattr(self, name)
|
|
except AttributeError:
|
|
pass
|
|
# END try attr deletion
|
|
# END for each name to delete
|
|
|
|
@classmethod
|
|
def _sio_modules(cls, parent_commit):
|
|
""":return: Configuration file as BytesIO - we only access it through the respective blob's data"""
|
|
sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
|
|
sio.name = cls.k_modules_file
|
|
return sio
|
|
|
|
def _config_parser_constrained(self, read_only):
|
|
""":return: Config Parser constrained to our submodule in read or write mode"""
|
|
try:
|
|
pc = self.parent_commit
|
|
except ValueError:
|
|
pc = None
|
|
# end handle empty parent repository
|
|
parser = self._config_parser(self.repo, pc, read_only)
|
|
parser.set_submodule(self)
|
|
return SectionConstraint(parser, sm_section(self.name))
|
|
|
|
@classmethod
|
|
def _module_abspath(cls, parent_repo, path, name):
|
|
if cls._need_gitfile_submodules(parent_repo.git):
|
|
return osp.join(parent_repo.git_dir, 'modules', name)
|
|
else:
|
|
return osp.join(parent_repo.working_tree_dir, path)
|
|
# end
|
|
|
|
@classmethod
|
|
def _clone_repo(cls, repo, url, path, name, **kwargs):
|
|
""":return: Repo instance of newly cloned repository
|
|
:param repo: our parent repository
|
|
:param url: url to clone from
|
|
:param path: repository-relative path to the submodule checkout location
|
|
:param name: canonical of the submodule
|
|
:param kwrags: additinoal arguments given to git.clone"""
|
|
module_abspath = cls._module_abspath(repo, path, name)
|
|
module_checkout_path = module_abspath
|
|
if cls._need_gitfile_submodules(repo.git):
|
|
kwargs['separate_git_dir'] = module_abspath
|
|
module_abspath_dir = osp.dirname(module_abspath)
|
|
if not osp.isdir(module_abspath_dir):
|
|
os.makedirs(module_abspath_dir)
|
|
module_checkout_path = osp.join(repo.working_tree_dir, path)
|
|
# end
|
|
|
|
clone = git.Repo.clone_from(url, module_checkout_path, **kwargs)
|
|
if cls._need_gitfile_submodules(repo.git):
|
|
cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
|
|
# end
|
|
return clone
|
|
|
|
@classmethod
|
|
def _to_relative_path(cls, parent_repo, path):
|
|
""":return: a path guaranteed to be relative to the given parent-repository
|
|
:raise ValueError: if path is not contained in the parent repository's working tree"""
|
|
path = to_native_path_linux(path)
|
|
if path.endswith('/'):
|
|
path = path[:-1]
|
|
# END handle trailing slash
|
|
|
|
if osp.isabs(path):
|
|
working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
|
|
if not path.startswith(working_tree_linux):
|
|
raise ValueError("Submodule checkout path '%s' needs to be within the parents repository at '%s'"
|
|
% (working_tree_linux, path))
|
|
path = path[len(working_tree_linux.rstrip('/')) + 1:]
|
|
if not path:
|
|
raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
|
|
# end verify converted relative path makes sense
|
|
# end convert to a relative path
|
|
|
|
return path
|
|
|
|
@classmethod
|
|
def _write_git_file_and_module_config(cls, working_tree_dir, module_abspath):
|
|
"""Writes a .git file containing a (preferably) relative path to the actual git module repository.
|
|
It is an error if the module_abspath cannot be made into a relative path, relative to the working_tree_dir
|
|
:note: will overwrite existing files !
|
|
:note: as we rewrite both the git file as well as the module configuration, we might fail on the configuration
|
|
and will not roll back changes done to the git file. This should be a non-issue, but may easily be fixed
|
|
if it becomes one
|
|
:param working_tree_dir: directory to write the .git file into
|
|
:param module_abspath: absolute path to the bare repository
|
|
"""
|
|
git_file = osp.join(working_tree_dir, '.git')
|
|
rela_path = osp.relpath(module_abspath, start=working_tree_dir)
|
|
if is_win:
|
|
if osp.isfile(git_file):
|
|
os.remove(git_file)
|
|
with open(git_file, 'wb') as fp:
|
|
fp.write(("gitdir: %s" % rela_path).encode(defenc))
|
|
|
|
with GitConfigParser(osp.join(module_abspath, 'config'),
|
|
read_only=False, merge_includes=False) as writer:
|
|
writer.set_value('core', 'worktree',
|
|
to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)))
|
|
|
|
#{ Edit Interface
|
|
|
|
@classmethod
|
|
def add(cls, repo, name, path, url=None, branch=None, no_checkout=False):
|
|
"""Add a new submodule to the given repository. This will alter the index
|
|
as well as the .gitmodules file, but will not create a new commit.
|
|
If the submodule already exists, no matter if the configuration differs
|
|
from the one provided, the existing submodule will be returned.
|
|
|
|
:param repo: Repository instance which should receive the submodule
|
|
:param name: The name/identifier for the submodule
|
|
:param path: repository-relative or absolute path at which the submodule
|
|
should be located
|
|
It will be created as required during the repository initialization.
|
|
:param url: git-clone compatible URL, see git-clone reference for more information
|
|
If None, the repository is assumed to exist, and the url of the first
|
|
remote is taken instead. This is useful if you want to make an existing
|
|
repository a submodule of anotherone.
|
|
:param branch: name of branch at which the submodule should (later) be checked out.
|
|
The given branch must exist in the remote repository, and will be checked
|
|
out locally as a tracking branch.
|
|
It will only be written into the configuration if it not None, which is
|
|
when the checked out branch will be the one the remote HEAD pointed to.
|
|
The result you get in these situation is somewhat fuzzy, and it is recommended
|
|
to specify at least 'master' here.
|
|
Examples are 'master' or 'feature/new'
|
|
:param no_checkout: if True, and if the repository has to be cloned manually,
|
|
no checkout will be performed
|
|
:return: The newly created submodule instance
|
|
:note: works atomically, such that no change will be done if the repository
|
|
update fails for instance"""
|
|
if repo.bare:
|
|
raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
|
|
# END handle bare repos
|
|
|
|
path = cls._to_relative_path(repo, path)
|
|
|
|
# assure we never put backslashes into the url, as some operating systems
|
|
# like it ...
|
|
if url is not None:
|
|
url = to_native_path_linux(url)
|
|
# END assure url correctness
|
|
|
|
# INSTANTIATE INTERMEDIATE SM
|
|
sm = cls(repo, cls.NULL_BIN_SHA, cls.k_default_mode, path, name, url='invalid-temporary')
|
|
if sm.exists():
|
|
# reretrieve submodule from tree
|
|
try:
|
|
sm = repo.head.commit.tree[path]
|
|
sm._name = name
|
|
return sm
|
|
except KeyError:
|
|
# could only be in index
|
|
index = repo.index
|
|
entry = index.entries[index.entry_key(path, 0)]
|
|
sm.binsha = entry.binsha
|
|
return sm
|
|
# END handle exceptions
|
|
# END handle existing
|
|
|
|
# fake-repo - we only need the functionality on the branch instance
|
|
br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default))
|
|
has_module = sm.module_exists()
|
|
branch_is_default = branch is None
|
|
if has_module and url is not None:
|
|
if url not in [r.url for r in sm.module().remotes]:
|
|
raise ValueError(
|
|
"Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath))
|
|
# END check url
|
|
# END verify urls match
|
|
|
|
mrepo = None
|
|
if url is None:
|
|
if not has_module:
|
|
raise ValueError("A URL was not given and existing repository did not exsit at %s" % path)
|
|
# END check url
|
|
mrepo = sm.module()
|
|
urls = [r.url for r in mrepo.remotes]
|
|
if not urls:
|
|
raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath)
|
|
# END verify we have url
|
|
url = urls[0]
|
|
else:
|
|
# clone new repo
|
|
kwargs = {'n': no_checkout}
|
|
if not branch_is_default:
|
|
kwargs['b'] = br.name
|
|
# END setup checkout-branch
|
|
|
|
# _clone_repo(cls, repo, url, path, name, **kwargs):
|
|
mrepo = cls._clone_repo(repo, url, path, name, **kwargs)
|
|
# END verify url
|
|
|
|
## See #525 for ensuring git urls in config-files valid under Windows.
|
|
url = Git.polish_url(url)
|
|
|
|
# It's important to add the URL to the parent config, to let `git submodule` know.
|
|
# otherwise there is a '-' character in front of the submodule listing
|
|
# a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
|
|
# -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
|
|
with sm.repo.config_writer() as writer:
|
|
writer.set_value(sm_section(name), 'url', url)
|
|
|
|
# update configuration and index
|
|
index = sm.repo.index
|
|
with sm.config_writer(index=index, write=False) as writer:
|
|
writer.set_value('url', url)
|
|
writer.set_value('path', path)
|
|
|
|
sm._url = url
|
|
if not branch_is_default:
|
|
# store full path
|
|
writer.set_value(cls.k_head_option, br.path)
|
|
sm._branch_path = br.path
|
|
|
|
# we deliberately assume that our head matches our index !
|
|
sm.binsha = mrepo.head.commit.binsha
|
|
index.add([sm], write=True)
|
|
|
|
return sm
|
|
|
|
def update(self, recursive=False, init=True, to_latest_revision=False, progress=None, dry_run=False,
|
|
force=False, keep_going=False):
|
|
"""Update the repository of this submodule to point to the checkout
|
|
we point at with the binsha of this instance.
|
|
|
|
:param recursive: if True, we will operate recursively and update child-
|
|
modules as well.
|
|
:param init: if True, the module repository will be cloned into place if necessary
|
|
:param to_latest_revision: if True, the submodule's sha will be ignored during checkout.
|
|
Instead, the remote will be fetched, and the local tracking branch updated.
|
|
This only works if we have a local tracking branch, which is the case
|
|
if the remote repository had a master branch, or of the 'branch' option
|
|
was specified for this submodule and the branch existed remotely
|
|
:param progress: UpdateProgress instance or None if no progress should be shown
|
|
:param dry_run: if True, the operation will only be simulated, but not performed.
|
|
All performed operations are read-only
|
|
:param force:
|
|
If True, we may reset heads even if the repository in question is dirty. Additinoally we will be allowed
|
|
to set a tracking branch which is ahead of its remote branch back into the past or the location of the
|
|
remote branch. This will essentially 'forget' commits.
|
|
If False, local tracking branches that are in the future of their respective remote branches will simply
|
|
not be moved.
|
|
:param keep_going: if True, we will ignore but log all errors, and keep going recursively.
|
|
Unless dry_run is set as well, keep_going could cause subsequent/inherited errors you wouldn't see
|
|
otherwise.
|
|
In conjunction with dry_run, it can be useful to anticipate all errors when updating submodules
|
|
:note: does nothing in bare repositories
|
|
:note: method is definitely not atomic if recurisve is True
|
|
:return: self"""
|
|
if self.repo.bare:
|
|
return self
|
|
# END pass in bare mode
|
|
|
|
if progress is None:
|
|
progress = UpdateProgress()
|
|
# END handle progress
|
|
prefix = ''
|
|
if dry_run:
|
|
prefix = "DRY-RUN: "
|
|
# END handle prefix
|
|
|
|
# to keep things plausible in dry-run mode
|
|
if dry_run:
|
|
mrepo = None
|
|
# END init mrepo
|
|
|
|
try:
|
|
# ASSURE REPO IS PRESENT AND UPTODATE
|
|
#####################################
|
|
try:
|
|
mrepo = self.module()
|
|
rmts = mrepo.remotes
|
|
len_rmts = len(rmts)
|
|
for i, remote in enumerate(rmts):
|
|
op = FETCH
|
|
if i == 0:
|
|
op |= BEGIN
|
|
# END handle start
|
|
|
|
progress.update(op, i, len_rmts, prefix + "Fetching remote %s of submodule %r"
|
|
% (remote, self.name))
|
|
#===============================
|
|
if not dry_run:
|
|
remote.fetch(progress=progress)
|
|
# END handle dry-run
|
|
#===============================
|
|
if i == len_rmts - 1:
|
|
op |= END
|
|
# END handle end
|
|
progress.update(op, i, len_rmts, prefix + "Done fetching remote of submodule %r" % self.name)
|
|
# END fetch new data
|
|
except InvalidGitRepositoryError:
|
|
if not init:
|
|
return self
|
|
# END early abort if init is not allowed
|
|
|
|
# there is no git-repository yet - but delete empty paths
|
|
checkout_module_abspath = self.abspath
|
|
if not dry_run and osp.isdir(checkout_module_abspath):
|
|
try:
|
|
os.rmdir(checkout_module_abspath)
|
|
except OSError:
|
|
raise OSError("Module directory at %r does already exist and is non-empty"
|
|
% checkout_module_abspath)
|
|
# END handle OSError
|
|
# END handle directory removal
|
|
|
|
# don't check it out at first - nonetheless it will create a local
|
|
# branch according to the remote-HEAD if possible
|
|
progress.update(BEGIN | CLONE, 0, 1, prefix + "Cloning url '%s' to '%s' in submodule %r" %
|
|
(self.url, checkout_module_abspath, self.name))
|
|
if not dry_run:
|
|
mrepo = self._clone_repo(self.repo, self.url, self.path, self.name, n=True)
|
|
# END handle dry-run
|
|
progress.update(END | CLONE, 0, 1, prefix + "Done cloning to %s" % checkout_module_abspath)
|
|
|
|
if not dry_run:
|
|
# see whether we have a valid branch to checkout
|
|
try:
|
|
# find a remote which has our branch - we try to be flexible
|
|
remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
|
|
local_branch = mkhead(mrepo, self.branch_path)
|
|
|
|
# have a valid branch, but no checkout - make sure we can figure
|
|
# that out by marking the commit with a null_sha
|
|
local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
|
|
# END initial checkout + branch creation
|
|
|
|
# make sure HEAD is not detached
|
|
mrepo.head.set_reference(local_branch, logmsg="submodule: attaching head to %s" % local_branch)
|
|
mrepo.head.ref.set_tracking_branch(remote_branch)
|
|
except (IndexError, InvalidGitRepositoryError):
|
|
log.warn("Failed to checkout tracking branch %s", self.branch_path)
|
|
# END handle tracking branch
|
|
|
|
# NOTE: Have to write the repo config file as well, otherwise
|
|
# the default implementation will be offended and not update the repository
|
|
# Maybe this is a good way to assure it doesn't get into our way, but
|
|
# we want to stay backwards compatible too ... . Its so redundant !
|
|
with self.repo.config_writer() as writer:
|
|
writer.set_value(sm_section(self.name), 'url', self.url)
|
|
# END handle dry_run
|
|
# END handle initialization
|
|
|
|
# DETERMINE SHAS TO CHECKOUT
|
|
############################
|
|
binsha = self.binsha
|
|
hexsha = self.hexsha
|
|
if mrepo is not None:
|
|
# mrepo is only set if we are not in dry-run mode or if the module existed
|
|
is_detached = mrepo.head.is_detached
|
|
# END handle dry_run
|
|
|
|
if mrepo is not None and to_latest_revision:
|
|
msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir
|
|
if not is_detached:
|
|
rref = mrepo.head.ref.tracking_branch()
|
|
if rref is not None:
|
|
rcommit = rref.commit
|
|
binsha = rcommit.binsha
|
|
hexsha = rcommit.hexsha
|
|
else:
|
|
log.error("%s a tracking branch was not set for local branch '%s'", msg_base, mrepo.head.ref)
|
|
# END handle remote ref
|
|
else:
|
|
log.error("%s there was no local tracking branch", msg_base)
|
|
# END handle detached head
|
|
# END handle to_latest_revision option
|
|
|
|
# update the working tree
|
|
# handles dry_run
|
|
if mrepo is not None and mrepo.head.commit.binsha != binsha:
|
|
# We must assure that our destination sha (the one to point to) is in the future of our current head.
|
|
# Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed
|
|
# We also handle the case that history has been rewritten, leaving no merge-base. In that case
|
|
# we behave conservatively, protecting possible changes the user had done
|
|
may_reset = True
|
|
if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
|
|
base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
|
|
if len(base_commit) == 0 or base_commit[0].hexsha == hexsha:
|
|
if force:
|
|
msg = "Will force checkout or reset on local branch that is possibly in the future of"
|
|
msg += "the commit it will be checked out to, effectively 'forgetting' new commits"
|
|
log.debug(msg)
|
|
else:
|
|
msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"
|
|
msg %= (is_detached and "checkout" or "reset", mrepo.head, mrepo)
|
|
log.info(msg)
|
|
may_reset = False
|
|
# end handle force
|
|
# end handle if we are in the future
|
|
|
|
if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True):
|
|
raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository")
|
|
# end handle force and dirty state
|
|
# end handle empty repo
|
|
|
|
# end verify future/past
|
|
progress.update(BEGIN | UPDWKTREE, 0, 1, prefix +
|
|
"Updating working tree at %s for submodule %r to revision %s"
|
|
% (self.path, self.name, hexsha))
|
|
|
|
if not dry_run and may_reset:
|
|
if is_detached:
|
|
# NOTE: for now we force, the user is no supposed to change detached
|
|
# submodules anyway. Maybe at some point this becomes an option, to
|
|
# properly handle user modifications - see below for future options
|
|
# regarding rebase and merge.
|
|
mrepo.git.checkout(hexsha, force=force)
|
|
else:
|
|
mrepo.head.reset(hexsha, index=True, working_tree=True)
|
|
# END handle checkout
|
|
# if we may reset/checkout
|
|
progress.update(END | UPDWKTREE, 0, 1, prefix + "Done updating working tree for submodule %r"
|
|
% self.name)
|
|
# END update to new commit only if needed
|
|
except Exception as err:
|
|
if not keep_going:
|
|
raise
|
|
log.error(str(err))
|
|
# end handle keep_going
|
|
|
|
# HANDLE RECURSION
|
|
##################
|
|
if recursive:
|
|
# in dry_run mode, the module might not exist
|
|
if mrepo is not None:
|
|
for submodule in self.iter_items(self.module()):
|
|
submodule.update(recursive, init, to_latest_revision, progress=progress, dry_run=dry_run,
|
|
force=force, keep_going=keep_going)
|
|
# END handle recursive update
|
|
# END handle dry run
|
|
# END for each submodule
|
|
|
|
return self
|
|
|
|
@unbare_repo
|
|
def move(self, module_path, configuration=True, module=True):
|
|
"""Move the submodule to a another module path. This involves physically moving
|
|
the repository at our current path, changing the configuration, as well as
|
|
adjusting our index entry accordingly.
|
|
|
|
:param module_path: the path to which to move our module in the parent repostory's working tree,
|
|
given as repository-relative or absolute path. Intermediate directories will be created
|
|
accordingly. If the path already exists, it must be empty.
|
|
Trailing (back)slashes are removed automatically
|
|
:param configuration: if True, the configuration will be adjusted to let
|
|
the submodule point to the given path.
|
|
:param module: if True, the repository managed by this submodule
|
|
will be moved as well. If False, we don't move the submodule's checkout, which may leave
|
|
the parent repository in an inconsistent state.
|
|
:return: self
|
|
:raise ValueError: if the module path existed and was not empty, or was a file
|
|
:note: Currently the method is not atomic, and it could leave the repository
|
|
in an inconsistent state if a sub-step fails for some reason
|
|
"""
|
|
if module + configuration < 1:
|
|
raise ValueError("You must specify to move at least the module or the configuration of the submodule")
|
|
# END handle input
|
|
|
|
module_checkout_path = self._to_relative_path(self.repo, module_path)
|
|
|
|
# VERIFY DESTINATION
|
|
if module_checkout_path == self.path:
|
|
return self
|
|
# END handle no change
|
|
|
|
module_checkout_abspath = join_path_native(self.repo.working_tree_dir, module_checkout_path)
|
|
if osp.isfile(module_checkout_abspath):
|
|
raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath)
|
|
# END handle target files
|
|
|
|
index = self.repo.index
|
|
tekey = index.entry_key(module_checkout_path, 0)
|
|
# if the target item already exists, fail
|
|
if configuration and tekey in index.entries:
|
|
raise ValueError("Index entry for target path did already exist")
|
|
# END handle index key already there
|
|
|
|
# remove existing destination
|
|
if module:
|
|
if osp.exists(module_checkout_abspath):
|
|
if len(os.listdir(module_checkout_abspath)):
|
|
raise ValueError("Destination module directory was not empty")
|
|
# END handle non-emptiness
|
|
|
|
if osp.islink(module_checkout_abspath):
|
|
os.remove(module_checkout_abspath)
|
|
else:
|
|
os.rmdir(module_checkout_abspath)
|
|
# END handle link
|
|
else:
|
|
# recreate parent directories
|
|
# NOTE: renames() does that now
|
|
pass
|
|
# END handle existence
|
|
# END handle module
|
|
|
|
# move the module into place if possible
|
|
cur_path = self.abspath
|
|
renamed_module = False
|
|
if module and osp.exists(cur_path):
|
|
os.renames(cur_path, module_checkout_abspath)
|
|
renamed_module = True
|
|
|
|
if osp.isfile(osp.join(module_checkout_abspath, '.git')):
|
|
module_abspath = self._module_abspath(self.repo, self.path, self.name)
|
|
self._write_git_file_and_module_config(module_checkout_abspath, module_abspath)
|
|
# end handle git file rewrite
|
|
# END move physical module
|
|
|
|
# rename the index entry - have to manipulate the index directly as
|
|
# git-mv cannot be used on submodules ... yeah
|
|
previous_sm_path = self.path
|
|
try:
|
|
if configuration:
|
|
try:
|
|
ekey = index.entry_key(self.path, 0)
|
|
entry = index.entries[ekey]
|
|
del(index.entries[ekey])
|
|
nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:])
|
|
index.entries[tekey] = nentry
|
|
except KeyError:
|
|
raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path))
|
|
# END handle submodule doesn't exist
|
|
|
|
# update configuration
|
|
with self.config_writer(index=index) as writer: # auto-write
|
|
writer.set_value('path', module_checkout_path)
|
|
self.path = module_checkout_path
|
|
# END handle configuration flag
|
|
except Exception:
|
|
if renamed_module:
|
|
os.renames(module_checkout_abspath, cur_path)
|
|
# END undo module renaming
|
|
raise
|
|
# END handle undo rename
|
|
|
|
# Auto-rename submodule if it's name was 'default', that is, the checkout directory
|
|
if previous_sm_path == self.name:
|
|
self.rename(module_checkout_path)
|
|
# end
|
|
|
|
return self
|
|
|
|
@unbare_repo
|
|
def remove(self, module=True, force=False, configuration=True, dry_run=False):
|
|
"""Remove this submodule from the repository. This will remove our entry
|
|
from the .gitmodules file and the entry in the .git/config file.
|
|
|
|
:param module: If True, the module checkout we point to will be deleted
|
|
as well. If the module is currently on a commit which is not part
|
|
of any branch in the remote, if the currently checked out branch
|
|
working tree, or untracked files,
|
|
is ahead of its tracking branch, if you have modifications in the
|
|
In case the removal of the repository fails for these reasons, the
|
|
submodule status will not have been altered.
|
|
If this submodule has child-modules on its own, these will be deleted
|
|
prior to touching the own module.
|
|
:param force: Enforces the deletion of the module even though it contains
|
|
modifications. This basically enforces a brute-force file system based
|
|
deletion.
|
|
:param configuration: if True, the submodule is deleted from the configuration,
|
|
otherwise it isn't. Although this should be enabled most of the times,
|
|
this flag enables you to safely delete the repository of your submodule.
|
|
:param dry_run: if True, we will not actually do anything, but throw the errors
|
|
we would usually throw
|
|
:return: self
|
|
:note: doesn't work in bare repositories
|
|
:note: doesn't work atomically, as failure to remove any part of the submodule will leave
|
|
an inconsistent state
|
|
:raise InvalidGitRepositoryError: thrown if the repository cannot be deleted
|
|
:raise OSError: if directories or files could not be removed"""
|
|
if not (module or configuration):
|
|
raise ValueError("Need to specify to delete at least the module, or the configuration")
|
|
# END handle parameters
|
|
|
|
# Recursively remove children of this submodule
|
|
nc = 0
|
|
for csm in self.children():
|
|
nc += 1
|
|
csm.remove(module, force, configuration, dry_run)
|
|
del(csm)
|
|
# end
|
|
if configuration and not dry_run and nc > 0:
|
|
# Assure we don't leave the parent repository in a dirty state, and commit our changes
|
|
# It's important for recursive, unforced, deletions to work as expected
|
|
self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
|
|
# end handle recursion
|
|
|
|
# DELETE REPOSITORY WORKING TREE
|
|
################################
|
|
if module and self.module_exists():
|
|
mod = self.module()
|
|
git_dir = mod.git_dir
|
|
if force:
|
|
# take the fast lane and just delete everything in our module path
|
|
# TODO: If we run into permission problems, we have a highly inconsistent
|
|
# state. Delete the .git folders last, start with the submodules first
|
|
mp = self.abspath
|
|
method = None
|
|
if osp.islink(mp):
|
|
method = os.remove
|
|
elif osp.isdir(mp):
|
|
method = rmtree
|
|
elif osp.exists(mp):
|
|
raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
|
|
# END handle brutal deletion
|
|
if not dry_run:
|
|
assert method
|
|
method(mp)
|
|
# END apply deletion method
|
|
else:
|
|
# verify we may delete our module
|
|
if mod.is_dirty(index=True, working_tree=True, untracked_files=True):
|
|
raise InvalidGitRepositoryError(
|
|
"Cannot delete module at %s with any modifications, unless force is specified"
|
|
% mod.working_tree_dir)
|
|
# END check for dirt
|
|
|
|
# figure out whether we have new commits compared to the remotes
|
|
# NOTE: If the user pulled all the time, the remote heads might
|
|
# not have been updated, so commits coming from the remote look
|
|
# as if they come from us. But we stay strictly read-only and
|
|
# don't fetch beforehand.
|
|
for remote in mod.remotes:
|
|
num_branches_with_new_commits = 0
|
|
rrefs = remote.refs
|
|
for rref in rrefs:
|
|
num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
|
|
# END for each remote ref
|
|
# not a single remote branch contained all our commits
|
|
if len(rrefs) and num_branches_with_new_commits == len(rrefs):
|
|
raise InvalidGitRepositoryError(
|
|
"Cannot delete module at %s as there are new commits" % mod.working_tree_dir)
|
|
# END handle new commits
|
|
# have to manually delete references as python's scoping is
|
|
# not existing, they could keep handles open ( on windows this is a problem )
|
|
if len(rrefs):
|
|
del(rref)
|
|
# END handle remotes
|
|
del(rrefs)
|
|
del(remote)
|
|
# END for each remote
|
|
|
|
# finally delete our own submodule
|
|
if not dry_run:
|
|
self._clear_cache()
|
|
wtd = mod.working_tree_dir
|
|
del(mod) # release file-handles (windows)
|
|
import gc
|
|
gc.collect()
|
|
try:
|
|
rmtree(wtd)
|
|
except Exception as ex:
|
|
if HIDE_WINDOWS_KNOWN_ERRORS:
|
|
raise SkipTest("FIXME: fails with: PermissionError\n %s", ex)
|
|
else:
|
|
raise
|
|
# END delete tree if possible
|
|
# END handle force
|
|
|
|
if not dry_run and osp.isdir(git_dir):
|
|
self._clear_cache()
|
|
try:
|
|
rmtree(git_dir)
|
|
except Exception as ex:
|
|
if HIDE_WINDOWS_KNOWN_ERRORS:
|
|
raise SkipTest("FIXME: fails with: PermissionError\n %s", ex)
|
|
else:
|
|
raise
|
|
# end handle separate bare repository
|
|
# END handle module deletion
|
|
|
|
# void our data not to delay invalid access
|
|
if not dry_run:
|
|
self._clear_cache()
|
|
|
|
# DELETE CONFIGURATION
|
|
######################
|
|
if configuration and not dry_run:
|
|
# first the index-entry
|
|
parent_index = self.repo.index
|
|
try:
|
|
del(parent_index.entries[parent_index.entry_key(self.path, 0)])
|
|
except KeyError:
|
|
pass
|
|
# END delete entry
|
|
parent_index.write()
|
|
|
|
# now git config - need the config intact, otherwise we can't query
|
|
# information anymore
|
|
with self.repo.config_writer() as writer:
|
|
writer.remove_section(sm_section(self.name))
|
|
|
|
with self.config_writer() as writer:
|
|
writer.remove_section()
|
|
# END delete configuration
|
|
|
|
return self
|
|
|
|
def set_parent_commit(self, commit, check=True):
|
|
"""Set this instance to use the given commit whose tree is supposed to
|
|
contain the .gitmodules blob.
|
|
|
|
:param commit:
|
|
Commit'ish reference pointing at the root_tree, or None to always point to the
|
|
most recent commit
|
|
:param check:
|
|
if True, relatively expensive checks will be performed to verify
|
|
validity of the submodule.
|
|
:raise ValueError: if the commit's tree didn't contain the .gitmodules blob.
|
|
:raise ValueError:
|
|
if the parent commit didn't store this submodule under the current path
|
|
:return: self"""
|
|
if commit is None:
|
|
self._parent_commit = None
|
|
return self
|
|
# end handle None
|
|
pcommit = self.repo.commit(commit)
|
|
pctree = pcommit.tree
|
|
if self.k_modules_file not in pctree:
|
|
raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file))
|
|
# END handle exceptions
|
|
|
|
prev_pc = self._parent_commit
|
|
self._parent_commit = pcommit
|
|
|
|
if check:
|
|
parser = self._config_parser(self.repo, self._parent_commit, read_only=True)
|
|
if not parser.has_section(sm_section(self.name)):
|
|
self._parent_commit = prev_pc
|
|
raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit))
|
|
# END handle submodule did not exist
|
|
# END handle checking mode
|
|
|
|
# update our sha, it could have changed
|
|
# If check is False, we might see a parent-commit that doesn't even contain the submodule anymore.
|
|
# in that case, mark our sha as being NULL
|
|
try:
|
|
self.binsha = pctree[self.path].binsha
|
|
except KeyError:
|
|
self.binsha = self.NULL_BIN_SHA
|
|
# end
|
|
|
|
self._clear_cache()
|
|
return self
|
|
|
|
@unbare_repo
|
|
def config_writer(self, index=None, write=True):
|
|
""":return: a config writer instance allowing you to read and write the data
|
|
belonging to this submodule into the .gitmodules file.
|
|
|
|
:param index: if not None, an IndexFile instance which should be written.
|
|
defaults to the index of the Submodule's parent repository.
|
|
:param write: if True, the index will be written each time a configuration
|
|
value changes.
|
|
:note: the parameters allow for a more efficient writing of the index,
|
|
as you can pass in a modified index on your own, prevent automatic writing,
|
|
and write yourself once the whole operation is complete
|
|
:raise ValueError: if trying to get a writer on a parent_commit which does not
|
|
match the current head commit
|
|
:raise IOError: If the .gitmodules file/blob could not be read"""
|
|
writer = self._config_parser_constrained(read_only=False)
|
|
if index is not None:
|
|
writer.config._index = index
|
|
writer.config._auto_write = write
|
|
return writer
|
|
|
|
@unbare_repo
|
|
def rename(self, new_name):
|
|
"""Rename this submodule
|
|
:note: This method takes care of renaming the submodule in various places, such as
|
|
|
|
* $parent_git_dir/config
|
|
* $working_tree_dir/.gitmodules
|
|
* (git >=v1.8.0: move submodule repository to new name)
|
|
|
|
As .gitmodules will be changed, you would need to make a commit afterwards. The changed .gitmodules file
|
|
will already be added to the index
|
|
|
|
:return: this submodule instance
|
|
"""
|
|
if self.name == new_name:
|
|
return self
|
|
|
|
# .git/config
|
|
with self.repo.config_writer() as pw:
|
|
# As we ourselves didn't write anything about submodules into the parent .git/config,
|
|
# we will not require it to exist, and just ignore missing entries.
|
|
if pw.has_section(sm_section(self.name)):
|
|
pw.rename_section(sm_section(self.name), sm_section(new_name))
|
|
|
|
# .gitmodules
|
|
with self.config_writer(write=True).config as cw:
|
|
cw.rename_section(sm_section(self.name), sm_section(new_name))
|
|
|
|
self._name = new_name
|
|
|
|
# .git/modules
|
|
mod = self.module()
|
|
if mod.has_separate_working_tree():
|
|
destination_module_abspath = self._module_abspath(self.repo, self.path, new_name)
|
|
source_dir = mod.git_dir
|
|
# Let's be sure the submodule name is not so obviously tied to a directory
|
|
if destination_module_abspath.startswith(mod.git_dir):
|
|
tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
|
|
os.renames(source_dir, tmp_dir)
|
|
source_dir = tmp_dir
|
|
# end handle self-containment
|
|
os.renames(source_dir, destination_module_abspath)
|
|
self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath)
|
|
# end move separate git repository
|
|
|
|
return self
|
|
|
|
#} END edit interface
|
|
|
|
#{ Query Interface
|
|
|
|
@unbare_repo
|
|
def module(self):
|
|
""":return: Repo instance initialized from the repository at our submodule path
|
|
:raise InvalidGitRepositoryError: if a repository was not available. This could
|
|
also mean that it was not yet initialized"""
|
|
# late import to workaround circular dependencies
|
|
module_checkout_abspath = self.abspath
|
|
try:
|
|
repo = git.Repo(module_checkout_abspath)
|
|
if repo != self.repo:
|
|
return repo
|
|
# END handle repo uninitialized
|
|
except (InvalidGitRepositoryError, NoSuchPathError):
|
|
raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath)
|
|
else:
|
|
raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath)
|
|
# END handle exceptions
|
|
|
|
def module_exists(self):
|
|
""":return: True if our module exists and is a valid git repository. See module() method"""
|
|
try:
|
|
self.module()
|
|
return True
|
|
except Exception:
|
|
return False
|
|
# END handle exception
|
|
|
|
def exists(self):
|
|
"""
|
|
:return: True if the submodule exists, False otherwise. Please note that
|
|
a submodule may exist (in the .gitmodules file) even though its module
|
|
doesn't exist on disk"""
|
|
# keep attributes for later, and restore them if we have no valid data
|
|
# this way we do not actually alter the state of the object
|
|
loc = locals()
|
|
for attr in self._cache_attrs:
|
|
try:
|
|
if hasattr(self, attr):
|
|
loc[attr] = getattr(self, attr)
|
|
# END if we have the attribute cache
|
|
except (cp.NoSectionError, ValueError):
|
|
# on PY3, this can happen apparently ... don't know why this doesn't happen on PY2
|
|
pass
|
|
# END for each attr
|
|
self._clear_cache()
|
|
|
|
try:
|
|
try:
|
|
self.path
|
|
return True
|
|
except Exception:
|
|
return False
|
|
# END handle exceptions
|
|
finally:
|
|
for attr in self._cache_attrs:
|
|
if attr in loc:
|
|
setattr(self, attr, loc[attr])
|
|
# END if we have a cache
|
|
# END reapply each attribute
|
|
# END handle object state consistency
|
|
|
|
@property
|
|
def branch(self):
|
|
""":return: The branch instance that we are to checkout
|
|
:raise InvalidGitRepositoryError: if our module is not yet checked out"""
|
|
return mkhead(self.module(), self._branch_path)
|
|
|
|
@property
|
|
def branch_path(self):
|
|
"""
|
|
:return: full (relative) path as string to the branch we would checkout
|
|
from the remote and track"""
|
|
return self._branch_path
|
|
|
|
@property
|
|
def branch_name(self):
|
|
""":return: the name of the branch, which is the shortest possible branch name"""
|
|
# use an instance method, for this we create a temporary Head instance
|
|
# which uses a repository that is available at least ( it makes no difference )
|
|
return git.Head(self.repo, self._branch_path).name
|
|
|
|
@property
|
|
def url(self):
|
|
""":return: The url to the repository which our module-repository refers to"""
|
|
return self._url
|
|
|
|
@property
|
|
def parent_commit(self):
|
|
""":return: Commit instance with the tree containing the .gitmodules file
|
|
:note: will always point to the current head's commit if it was not set explicitly"""
|
|
if self._parent_commit is None:
|
|
return self.repo.commit()
|
|
return self._parent_commit
|
|
|
|
@property
|
|
def name(self):
|
|
""":return: The name of this submodule. It is used to identify it within the
|
|
.gitmodules file.
|
|
:note: by default, the name is the path at which to find the submodule, but
|
|
in git-python it should be a unique identifier similar to the identifiers
|
|
used for remotes, which allows to change the path of the submodule
|
|
easily
|
|
"""
|
|
return self._name
|
|
|
|
def config_reader(self):
|
|
"""
|
|
:return: ConfigReader instance which allows you to qurey the configuration values
|
|
of this submodule, as provided by the .gitmodules file
|
|
:note: The config reader will actually read the data directly from the repository
|
|
and thus does not need nor care about your working tree.
|
|
:note: Should be cached by the caller and only kept as long as needed
|
|
:raise IOError: If the .gitmodules file/blob could not be read"""
|
|
return self._config_parser_constrained(read_only=True)
|
|
|
|
def children(self):
|
|
"""
|
|
:return: IterableList(Submodule, ...) an iterable list of submodules instances
|
|
which are children of this submodule or 0 if the submodule is not checked out"""
|
|
return self._get_intermediate_items(self)
|
|
|
|
#} END query interface
|
|
|
|
#{ Iterable Interface
|
|
|
|
@classmethod
|
|
def iter_items(cls, repo, parent_commit='HEAD'):
|
|
""":return: iterator yielding Submodule instances available in the given repository"""
|
|
pc = repo.commit(parent_commit) # parent commit instance
|
|
try:
|
|
parser = cls._config_parser(repo, pc, read_only=True)
|
|
except IOError:
|
|
raise StopIteration
|
|
# END handle empty iterator
|
|
|
|
rt = pc.tree # root tree
|
|
|
|
for sms in parser.sections():
|
|
n = sm_name(sms)
|
|
p = parser.get(sms, 'path')
|
|
u = parser.get(sms, 'url')
|
|
b = cls.k_head_default
|
|
if parser.has_option(sms, cls.k_head_option):
|
|
b = str(parser.get(sms, cls.k_head_option))
|
|
# END handle optional information
|
|
|
|
# get the binsha
|
|
index = repo.index
|
|
try:
|
|
sm = rt[p]
|
|
except KeyError:
|
|
# try the index, maybe it was just added
|
|
try:
|
|
entry = index.entries[index.entry_key(p, 0)]
|
|
sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
|
|
except KeyError:
|
|
raise InvalidGitRepositoryError(
|
|
"Gitmodule path %r did not exist in revision of parent commit %s" % (p, parent_commit))
|
|
# END handle keyerror
|
|
# END handle critical error
|
|
|
|
# fill in remaining info - saves time as it doesn't have to be parsed again
|
|
sm._name = n
|
|
if pc != repo.commit():
|
|
sm._parent_commit = pc
|
|
# end set only if not most recent !
|
|
sm._branch_path = git.Head.to_full_path(b)
|
|
sm._url = u
|
|
|
|
yield sm
|
|
# END for each section
|
|
|
|
#} END iterable interface
|