Compare commits

...

29 Commits

Author SHA1 Message Date
37dbf2bf0f Try to prevent 'repo sync' as a user name
When someone copies and pastes a setup line from a web page,
they might actually copy 'repo sync' onto the clipboard and wind
up pasting it into the "Your Name" prompt.  This means they will
initialize their client with the user name of "repo sync", creating
some rather funny looking commits later on.  For example:

  To setup your source tree:

    mkdir ~/code
    cd ~/code
    repo init -u git://....
    repo sync

If this entire block was just blindly copy and pasted into the
terminal, the shell won't read "repo sync" but "repo init" will.

By showing the user their full identity string, and asking them
to confirm it before we continue, we can give the hapless user a
chance to recover from this mistake, without unfairly harming those
who were actually named 'repo' by their parents.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-07-02 10:53:04 -07:00
438c54713a git_config: handle configuration entries with no values
A git-config entry with no value was preventing repo
from initializing.  This modifies _ReadGit() to handle
config entries with empty values.

Signed-off-by: David Aguilar <davvid@gmail.com>
Reported-by: Josh Guilfoyle <jasta00@gmail.com>
2009-06-29 00:24:36 -07:00
e020ebee4e .gitignore: add an entry for repopickles
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-06-28 15:08:56 -07:00
21c5c34ee2 Support detached HEAD in manifest repository
If the manifest repository is on a detached HEAD and we are parsing
an XML formatted manifest we should simply set the branch property
to None, rather than crash with an AttributeError.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-25 16:47:30 -07:00
54fccd71fb Document any crashes from the user's text editor
Rather than failing with no information, display the child exit
status and the command line we tried to use to edit a text file.
There may be some useful information to help understand the crash.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-24 07:15:21 -07:00
fb5c8fd948 Fix invalid use of try-catch
Its try-except in Python.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-16 14:59:19 -07:00
26120ca18d Don't crash if the ssh client is already dead
If the SSH client terminated abnormally in the background (e.g. the
server shutdown while we were doing a sync) then the pid won't exist.
Instead of crashing, ignore it, the result we wanted (a non-orphaned
ssh process) is already acheived.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-16 11:49:10 -07:00
7da73d6f3b branches: Describe output format in repo help branches
Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-12 17:35:43 -07:00
f0d4c36701 grep: Only use --color on git 1.6.3 and later
The --color flag wasn't introduced until git 1.6.3.  Prior to that
version, `git grep --color` just produces a fatal error, as it is
an unsupported option.  Since this is just pretty output and is not
critical to execution, we can simply omit the option if the version
of git we are running on doesn't support it.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-12 09:33:48 -07:00
2ec00b9272 Refactor git version detection for reuse
This way we can use it to detect feature support in the underlying
git, such as new options or commands that have been added in more
recent versions.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-12 09:32:50 -07:00
2a3a81b51f Ignore EOFError when reading a truncated pickle file
If the pickle config file is 0 bytes in length,  we may have
crashed (or been aborted) while writing the file out to disk.
Instead of crashing with a backtrace, just treat the file as
though it wasn't present and load off a `git config` fork.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-12 09:10:07 -07:00
7b4f43542a Add missing return False to preconnect
Noticed by users on repo-discuss, we were missing a return False
here to signal that SSH control master was not used to setup the
network connection.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-12 09:08:34 -07:00
9fb29ce123 sync: Keep the project.list file sorted
Its easier to locate an entry visually if the file is sorted.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-04 20:41:26 -07:00
3a68bb4c7f sync: Tolerate blank lines in project.list
If a line is blank in project.list, its not a relevant project path,
so skip over it.  Existing project.list files may have blank lines if
sync was run with no projects at all, and the file was created empty.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-04 16:21:01 -07:00
cd1d7ff81e sync: Don't process project.list in a mirror
We have no working tree, so we cannot update the project.list
state file, nor should we try to delete a directory if a project is
removed from the manifest.  Clients would still need the repository
for historical records.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-04 16:20:02 -07:00
da88ff4411 Silence 'Current branch %s is up to date' during sync
We accidentally introduced this message during 1.6.8 by always
invoking `git rebase` when there were no new commits from the
upstream, but the user had local commits.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-03 11:09:31 -07:00
8135cdc53c Delete empty parent subdirs after deleting obsolete paths.
After sync, we delete obsolete project paths.
Iterate and delete parent subdirs which are empty.
Tested on projects within subdirectories.
2009-06-02 15:08:45 -07:00
4f2517ff11 Update project paths after sync.
After a repo sync, some of the project paths might need
to be removed. This changes maintains a list of project
paths from the previous sync operation and compares.
2009-06-02 11:00:53 -07:00
fe200eeb52 Fix unnecessary self in project.py
Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-06-01 15:28:21 -07:00
078a8b270f Add PyDev project files to repo 2009-06-02 00:09:07 +02:00
3c8dea1f8d Change project.revision to revisionExpr and revisionId
The revisionExpr field now holds an expression from the manifest,
such as "refs/heads/master", while revisionId holds the current
commit-ish SHA-1 of the revisionExpr.  Currently that is only
filled in if the manifest points directly to a SHA-1.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-29 18:45:20 -07:00
8ad8a0e61d Change DWIMery hack for dealing with rewound remote branch
The trick of looking at the reflog for the remote tracking branch
and only going back one commit works some of the time, but not all of
the time.  Its sort of relying on the fact that the user didn't use
`repo sync -n` or `git fetch` to only update the tracking branches
and skip the working directory update.

Doing this right requires looking through the history of the SHA-1
source (what the upstream used to be) and finding a spot where the
DAG diveraged away suddenly, and consider that to be the rewind
point.  That's really difficult to do, as we don't have a clear
picture of what that old point was.

A close approximation is to list all of the commits that are in
HEAD, but not the new upstream, and rebase all of those where the
committer email address is this user's email address.  In most cases,
this will effectively rebase only the user's new original work.

If the user is the project maintainer and rewound the branch
themselves, and they don't want all of the commits they have created
to be rebased onto the new upstream, they should handle the rebase
on their own, after the sync is complete.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-29 18:45:17 -07:00
d1f70d9929 Refactor how projects parse remotes so it can be replaced
We now feed Project a RemoteSpec, instead of the Remote directly
from the XmlManifest.  This way the RemoteSpec already has the
full project URL, rather than just the base, permitting other
types of manifests to produce the URL in their own style.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-29 09:31:28 -07:00
c8a300f639 Refactor Manifest to be XmlManifest
We'll soon be supporting two different manifest formats, but we
can't immediately remove support for the current XML one that is
in wide spread use within Android.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-29 09:31:28 -07:00
1b34c9118e Allow callers of GitConfig to specify the pickle file path
This way we can put it in another directory than the config file
itself, e.g. hide it inside ".git" when parsing a ".gitmodules"
file from the working tree.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-29 09:31:00 -07:00
366ad214b8 Teach GitConfig how to yield subsection names
This can be useful when pulling apart a configuration file, like
finding all entries which match submodule.*.*.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-19 13:02:00 -07:00
242b52690d Remove support for the extra <remote> definitions in manifests
These aren't that widely used, and actually make it difficult for
users to fully mirror a forest of repositories, and then permit
someone else to clone off that forest, rather then the original
upstream servers.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-19 13:01:52 -07:00
4cc70ce501 Remove unused parsing support for <require commit=""/>
We haven't supported this in a while, but the parser was still here.
Its all dead code, so strip it out.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-19 13:01:48 -07:00
498a0e8a79 Make 'repo branches -a' the default behavior
Extensive discussion with users lead to the fact that needing to
supply -a to view what they really wanted to see was just wrong.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-05-18 12:28:57 -07:00
20 changed files with 446 additions and 343 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.pyc *.pyc
.repopickle_*

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>repo</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

10
.pydevproject Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?>
<pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/repo</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.4</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>

View File

@ -23,32 +23,23 @@ following DTD:
<!ELEMENT manifest (remote*, <!ELEMENT manifest (remote*,
default?, default?,
remove-project*, remove-project*,
project*, project*)>
add-remote*)>
<!ELEMENT remote (EMPTY)> <!ELEMENT remote (EMPTY)>
<!ATTLIST remote name ID #REQUIRED> <!ATTLIST remote name ID #REQUIRED>
<!ATTLIST remote fetch CDATA #REQUIRED> <!ATTLIST remote fetch CDATA #REQUIRED>
<!ATTLIST remote review CDATA #IMPLIED> <!ATTLIST remote review CDATA #IMPLIED>
<!ATTLIST remote project-name CDATA #IMPLIED>
<!ELEMENT default (EMPTY)> <!ELEMENT default (EMPTY)>
<!ATTLIST default remote IDREF #IMPLIED> <!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED> <!ATTLIST default revision CDATA #IMPLIED>
<!ELEMENT project (remote*)> <!ELEMENT project (EMPTY)>
<!ATTLIST project name CDATA #REQUIRED> <!ATTLIST project name CDATA #REQUIRED>
<!ATTLIST project path CDATA #IMPLIED> <!ATTLIST project path CDATA #IMPLIED>
<!ATTLIST project remote IDREF #IMPLIED> <!ATTLIST project remote IDREF #IMPLIED>
<!ATTLIST project revision CDATA #IMPLIED> <!ATTLIST project revision CDATA #IMPLIED>
<!ELEMENT add-remote (EMPTY)>
<!ATTLIST add-remote to-project ID #REQUIRED>
<!ATTLIST add-remote name ID #REQUIRED>
<!ATTLIST add-remote fetch CDATA #REQUIRED>
<!ATTLIST add-remote review CDATA #IMPLIED>
<!ATTLIST add-remote project-name CDATA #IMPLIED>
<!ELEMENT remove-project (EMPTY)> <!ELEMENT remove-project (EMPTY)>
<!ATTLIST remove-project name CDATA #REQUIRED> <!ATTLIST remove-project name CDATA #REQUIRED>
]> ]>
@ -82,25 +73,6 @@ Attribute `review`: Hostname of the Gerrit server where reviews
are uploaded to by `repo upload`. This attribute is optional; are uploaded to by `repo upload`. This attribute is optional;
if not specified then `repo upload` will not function. if not specified then `repo upload` will not function.
Attribute `project-name`: Specifies the name of this project used
by the review server given in the review attribute of this element.
Only permitted when the remote element is nested inside of a project
element (see below). If not given, defaults to the name supplied
in the project's name attribute.
Element add-remote
------------------
Adds a remote to an existing project, whose name is given by the
to-project attribute. This is functionally equivalent to nesting
a remote element under the project, but has the advantage that it
can be specified in the uesr's `local_manifest.xml` to add a remote
to a project declared by the normal manifest.
The element can be used to add a fork of an existing project that
the user needs to work with.
Element default Element default
--------------- ---------------
@ -152,13 +124,6 @@ Tags and/or explicit SHA-1s should work in theory, but have not
been extensively tested. If not supplied the revision given by been extensively tested. If not supplied the revision given by
the default element is used. the default element is used.
Child element `remote`: Described like the top-level remote element,
but adds an additional remote to only this project. These additional
remotes are fetched from first on the initial `repo sync`, causing
the majority of the project's object database to be obtained through
these additional remotes.
Element remove-project Element remove-project
---------------------- ----------------------

View File

@ -76,8 +76,15 @@ least one of these before using this command."""
os.close(fd) os.close(fd)
fd = None fd = None
if subprocess.Popen(editor + [path]).wait() != 0: try:
raise EditorError() rc = subprocess.Popen(editor + [path]).wait()
except OSError, e:
raise EditorError('editor failed, %s: %s %s'
% (str(e), cls._GetEditor(), path))
if rc != 0:
raise EditorError('editor failed with exit status %d: %s %s'
% (rc, cls._GetEditor(), path))
fd2 = open(path) fd2 = open(path)
try: try:
return fd2.read() return fd2.read()

View File

@ -24,6 +24,11 @@ class ManifestInvalidRevisionError(Exception):
class EditorError(Exception): class EditorError(Exception):
"""Unspecified error from the user's text editor. """Unspecified error from the user's text editor.
""" """
def __init__(self, reason):
self.reason = reason
def __str__(self):
return self.reason
class GitError(Exception): class GitError(Exception):
"""Unspecified internal error from git. """Unspecified internal error from git.

View File

@ -68,6 +68,30 @@ class _GitCall(object):
return fun return fun
git = _GitCall() git = _GitCall()
_git_version = None
def git_require(min_version, fail=False):
global _git_version
if _git_version is None:
ver_str = git.version()
if ver_str.startswith('git version '):
_git_version = tuple(
map(lambda x: int(x),
ver_str[len('git version '):].strip().split('.')[0:3]
))
else:
print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
sys.exit(1)
if min_version <= _git_version:
return True
if fail:
need = '.'.join(map(lambda x: str(x), min_version))
print >>sys.stderr, 'fatal: git %s or later required' % need
sys.exit(1)
return False
class GitCommand(object): class GitCommand(object):
def __init__(self, def __init__(self,
project, project,

View File

@ -56,16 +56,20 @@ class GitConfig(object):
return cls(file = os.path.join(gitdir, 'config'), return cls(file = os.path.join(gitdir, 'config'),
defaults = defaults) defaults = defaults)
def __init__(self, file, defaults=None): def __init__(self, file, defaults=None, pickleFile=None):
self.file = file self.file = file
self.defaults = defaults self.defaults = defaults
self._cache_dict = None self._cache_dict = None
self._section_dict = None self._section_dict = None
self._remotes = {} self._remotes = {}
self._branches = {} self._branches = {}
self._pickle = os.path.join(
os.path.dirname(self.file), if pickleFile is None:
'.repopickle_' + os.path.basename(self.file)) self._pickle = os.path.join(
os.path.dirname(self.file),
'.repopickle_' + os.path.basename(self.file))
else:
self._pickle = pickleFile
def Has(self, name, include_defaults = True): def Has(self, name, include_defaults = True):
"""Return true if this configuration file has the key. """Return true if this configuration file has the key.
@ -172,6 +176,11 @@ class GitConfig(object):
self._branches[b.name] = b self._branches[b.name] = b
return b return b
def GetSubSections(self, section):
"""List all subsection names matching $section.*.*
"""
return self._sections.get(section, set())
def HasSection(self, section, subsection = ''): def HasSection(self, section, subsection = ''):
"""Does at least one key in section.subsection exist? """Does at least one key in section.subsection exist?
""" """
@ -227,6 +236,9 @@ class GitConfig(object):
return cPickle.load(fd) return cPickle.load(fd)
finally: finally:
fd.close() fd.close()
except EOFError:
os.remove(self._pickle)
return None
except IOError: except IOError:
os.remove(self._pickle) os.remove(self._pickle)
return None return None
@ -247,21 +259,26 @@ class GitConfig(object):
os.remove(self._pickle) os.remove(self._pickle)
def _ReadGit(self): def _ReadGit(self):
d = self._do('--null', '--list') """
c = {} Read configuration data from git.
while d:
lf = d.index('\n')
nul = d.index('\0', lf + 1)
key = _key(d[0:lf]) This internal method populates the GitConfig cache.
val = d[lf + 1:nul]
"""
d = self._do('--null', '--list').rstrip('\0')
c = {}
for line in d.split('\0'):
if '\n' in line:
key, val = line.split('\n', 1)
else:
key = line
val = None
if key in c: if key in c:
c[key].append(val) c[key].append(val)
else: else:
c[key] = [val] c[key] = [val]
d = d[nul + 1:]
return c return c
def _do(self, *args): def _do(self, *args):
@ -373,8 +390,11 @@ def _open_ssh(host, port):
def close_ssh(): def close_ssh():
for key,p in _ssh_cache.iteritems(): for key,p in _ssh_cache.iteritems():
os.kill(p.pid, SIGTERM) try:
p.wait() os.kill(p.pid, SIGTERM)
p.wait()
except OSError:
pass
_ssh_cache.clear() _ssh_cache.clear()
d = _ssh_sock(create=False) d = _ssh_sock(create=False)
@ -405,6 +425,7 @@ def _preconnect(url):
host = m.group(1) host = m.group(1)
return _open_ssh(host, 22) return _open_ssh(host, 22)
return False
class Remote(object): class Remote(object):
"""Configuration options related to a remote. """Configuration options related to a remote.

View File

@ -36,7 +36,7 @@ from editor import Editor
from error import ManifestInvalidRevisionError from error import ManifestInvalidRevisionError
from error import NoSuchProjectError from error import NoSuchProjectError
from error import RepoChangedException from error import RepoChangedException
from manifest import Manifest from manifest_xml import XmlManifest
from pager import RunPager from pager import RunPager
from subcmds import all as all_commands from subcmds import all as all_commands
@ -97,7 +97,7 @@ class _Repo(object):
sys.exit(1) sys.exit(1)
cmd.repodir = self.repodir cmd.repodir = self.repodir
cmd.manifest = Manifest(cmd.repodir) cmd.manifest = XmlManifest(cmd.repodir)
Editor.globalConfig = cmd.manifest.globalConfig Editor.globalConfig = cmd.manifest.globalConfig
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror: if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:

View File

@ -18,8 +18,7 @@ import sys
import xml.dom.minidom import xml.dom.minidom
from git_config import GitConfig, IsId from git_config import GitConfig, IsId
from project import Project, MetaProject, R_HEADS, HEAD from project import RemoteSpec, Project, MetaProject, R_HEADS, HEAD
from remote import Remote
from error import ManifestParseError from error import ManifestParseError
MANIFEST_FILE_NAME = 'manifest.xml' MANIFEST_FILE_NAME = 'manifest.xml'
@ -28,11 +27,26 @@ LOCAL_MANIFEST_NAME = 'local_manifest.xml'
class _Default(object): class _Default(object):
"""Project defaults within the manifest.""" """Project defaults within the manifest."""
revision = None revisionExpr = None
remote = None remote = None
class _XmlRemote(object):
def __init__(self,
name,
fetch=None,
review=None):
self.name = name
self.fetchUrl = fetch
self.reviewUrl = review
class Manifest(object): def ToRemoteSpec(self, projectName):
url = self.fetchUrl
while url.endswith('/'):
url = url[:-1]
url += '/%s.git' % projectName
return RemoteSpec(self.name, url, self.reviewUrl)
class XmlManifest(object):
"""manages the repo configuration file""" """manages the repo configuration file"""
def __init__(self, repodir): def __init__(self, repodir):
@ -80,8 +94,6 @@ class Manifest(object):
e.setAttribute('fetch', r.fetchUrl) e.setAttribute('fetch', r.fetchUrl)
if r.reviewUrl is not None: if r.reviewUrl is not None:
e.setAttribute('review', r.reviewUrl) e.setAttribute('review', r.reviewUrl)
if r.projectName is not None:
e.setAttribute('project-name', r.projectName)
def Save(self, fd, peg_rev=False): def Save(self, fd, peg_rev=False):
"""Write the current manifest out to the given file descriptor. """Write the current manifest out to the given file descriptor.
@ -104,9 +116,9 @@ class Manifest(object):
if d.remote: if d.remote:
have_default = True have_default = True
e.setAttribute('remote', d.remote.name) e.setAttribute('remote', d.remote.name)
if d.revision: if d.revisionExpr:
have_default = True have_default = True
e.setAttribute('revision', d.revision) e.setAttribute('revision', d.revisionExpr)
if have_default: if have_default:
root.appendChild(e) root.appendChild(e)
root.appendChild(doc.createTextNode('')) root.appendChild(doc.createTextNode(''))
@ -126,15 +138,13 @@ class Manifest(object):
if peg_rev: if peg_rev:
if self.IsMirror: if self.IsMirror:
e.setAttribute('revision', e.setAttribute('revision',
p.bare_git.rev_parse(p.revision + '^0')) p.bare_git.rev_parse(p.revisionExpr + '^0'))
else: else:
e.setAttribute('revision', e.setAttribute('revision',
p.work_git.rev_parse(HEAD + '^0')) p.work_git.rev_parse(HEAD + '^0'))
elif not d.revision or p.revision != d.revision: elif not d.revisionExpr or p.revisionExpr != d.revisionExpr:
e.setAttribute('revision', p.revision) e.setAttribute('revision', p.revisionExpr)
for r in p.extraRemotes:
self._RemoteToXml(p.extraRemotes[r], doc, e)
for c in p.copyfiles: for c in p.copyfiles:
ce = doc.createElement('copyfile') ce = doc.createElement('copyfile')
ce.setAttribute('src', c.src) ce.setAttribute('src', c.src)
@ -173,7 +183,7 @@ class Manifest(object):
if not self._loaded: if not self._loaded:
m = self.manifestProject m = self.manifestProject
b = m.GetBranch(m.CurrentBranch).merge b = m.GetBranch(m.CurrentBranch).merge
if b.startswith(R_HEADS): if b is not None and b.startswith(R_HEADS):
b = b[len(R_HEADS):] b = b[len(R_HEADS):]
self.branch = b self.branch = b
@ -245,16 +255,6 @@ class Manifest(object):
(project.name, self.manifestFile) (project.name, self.manifestFile)
self._projects[project.name] = project self._projects[project.name] = project
for node in config.childNodes:
if node.nodeName == 'add-remote':
pn = self._reqatt(node, 'to-project')
project = self._projects.get(pn)
if not project:
raise ManifestParseError, \
'project %s not defined in %s' % \
(pn, self.manifestFile)
self._ParseProjectExtraRemote(project, node)
def _AddMetaProjectMirror(self, m): def _AddMetaProjectMirror(self, m):
name = None name = None
m_url = m.GetRemote(m.remote.name).url m_url = m.GetRemote(m.remote.name).url
@ -271,7 +271,7 @@ class Manifest(object):
if name is None: if name is None:
s = m_url.rindex('/') + 1 s = m_url.rindex('/') + 1
remote = Remote('origin', fetch = m_url[:s]) remote = _XmlRemote('origin', m_url[:s])
name = m_url[s:] name = m_url[s:]
if name.endswith('.git'): if name.endswith('.git'):
@ -282,11 +282,12 @@ class Manifest(object):
gitdir = os.path.join(self.topdir, '%s.git' % name) gitdir = os.path.join(self.topdir, '%s.git' % name)
project = Project(manifest = self, project = Project(manifest = self,
name = name, name = name,
remote = remote, remote = remote.ToRemoteSpec(name),
gitdir = gitdir, gitdir = gitdir,
worktree = None, worktree = None,
relpath = None, relpath = None,
revision = m.revision) revisionExpr = m.revisionExpr,
revisionId = None)
self._projects[project.name] = project self._projects[project.name] = project
def _ParseRemote(self, node): def _ParseRemote(self, node):
@ -298,21 +299,7 @@ class Manifest(object):
review = node.getAttribute('review') review = node.getAttribute('review')
if review == '': if review == '':
review = None review = None
return _XmlRemote(name, fetch, review)
projectName = node.getAttribute('project-name')
if projectName == '':
projectName = None
r = Remote(name=name,
fetch=fetch,
review=review,
projectName=projectName)
for n in node.childNodes:
if n.nodeName == 'require':
r.requiredCommits.append(self._reqatt(n, 'commit'))
return r
def _ParseDefault(self, node): def _ParseDefault(self, node):
""" """
@ -320,9 +307,9 @@ class Manifest(object):
""" """
d = _Default() d = _Default()
d.remote = self._get_remote(node) d.remote = self._get_remote(node)
d.revision = node.getAttribute('revision') d.revisionExpr = node.getAttribute('revision')
if d.revision == '': if d.revisionExpr == '':
d.revision = None d.revisionExpr = None
return d return d
def _ParseProject(self, node): def _ParseProject(self, node):
@ -339,10 +326,10 @@ class Manifest(object):
"no remote for project %s within %s" % \ "no remote for project %s within %s" % \
(name, self.manifestFile) (name, self.manifestFile)
revision = node.getAttribute('revision') revisionExpr = node.getAttribute('revision')
if not revision: if not revisionExpr:
revision = self._default.revision revisionExpr = self._default.revisionExpr
if not revision: if not revisionExpr:
raise ManifestParseError, \ raise ManifestParseError, \
"no revision for project %s within %s" % \ "no revision for project %s within %s" % \
(name, self.manifestFile) (name, self.manifestFile)
@ -365,29 +352,19 @@ class Manifest(object):
project = Project(manifest = self, project = Project(manifest = self,
name = name, name = name,
remote = remote, remote = remote.ToRemoteSpec(name),
gitdir = gitdir, gitdir = gitdir,
worktree = worktree, worktree = worktree,
relpath = path, relpath = path,
revision = revision) revisionExpr = revisionExpr,
revisionId = None)
for n in node.childNodes: for n in node.childNodes:
if n.nodeName == 'remote': if n.nodeName == 'copyfile':
self._ParseProjectExtraRemote(project, n)
elif n.nodeName == 'copyfile':
self._ParseCopyFile(project, n) self._ParseCopyFile(project, n)
return project return project
def _ParseProjectExtraRemote(self, project, n):
r = self._ParseRemote(n)
if project.extraRemotes.get(r.name) \
or project.remote.name == r.name:
raise ManifestParseError, \
'duplicate remote %s in project %s in %s' % \
(r.name, project.name, self.manifestFile)
project.extraRemotes[r.name] = r
def _ParseCopyFile(self, project, node): def _ParseCopyFile(self, project, node):
src = self._reqatt(node, 'src') src = self._reqatt(node, 'src')
dest = self._reqatt(node, 'dest') dest = self._reqatt(node, 'dest')

View File

@ -26,7 +26,6 @@ from git_command import GitCommand
from git_config import GitConfig, IsId from git_config import GitConfig, IsId
from error import GitError, ImportError, UploadError from error import GitError, ImportError, UploadError
from error import ManifestInvalidRevisionError from error import ManifestInvalidRevisionError
from remote import Remote
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
@ -212,6 +211,14 @@ class _CopyFile:
except IOError: except IOError:
_error('Cannot copy file %s to %s', src, dest) _error('Cannot copy file %s to %s', src, dest)
class RemoteSpec(object):
def __init__(self,
name,
url = None,
review = None):
self.name = name
self.url = url
self.review = review
class Project(object): class Project(object):
def __init__(self, def __init__(self,
@ -221,16 +228,24 @@ class Project(object):
gitdir, gitdir,
worktree, worktree,
relpath, relpath,
revision): revisionExpr,
revisionId):
self.manifest = manifest self.manifest = manifest
self.name = name self.name = name
self.remote = remote self.remote = remote
self.gitdir = gitdir self.gitdir = gitdir
self.worktree = worktree self.worktree = worktree
self.relpath = relpath self.relpath = relpath
self.revision = revision self.revisionExpr = revisionExpr
if revisionId is None \
and revisionExpr \
and IsId(revisionExpr):
self.revisionId = revisionExpr
else:
self.revisionId = revisionId
self.snapshots = {} self.snapshots = {}
self.extraRemotes = {}
self.copyfiles = [] self.copyfiles = []
self.config = GitConfig.ForRepository( self.config = GitConfig.ForRepository(
gitdir = self.gitdir, gitdir = self.gitdir,
@ -579,9 +594,6 @@ class Project(object):
self._InitGitDir() self._InitGitDir()
self._InitRemote() self._InitRemote()
for r in self.extraRemotes.values():
if not self._RemoteFetch(r.name):
return False
if not self._RemoteFetch(): if not self._RemoteFetch():
return False return False
@ -602,6 +614,23 @@ class Project(object):
for file in self.copyfiles: for file in self.copyfiles:
file._Copy() file._Copy()
def GetRevisionId(self, all=None):
if self.revisionId:
return self.revisionId
rem = self.GetRemote(self.remote.name)
rev = rem.ToLocal(self.revisionExpr)
if all is not None and rev in all:
return all[rev]
try:
return self.bare_git.rev_parse('--verify', '%s^0' % rev)
except GitError:
raise ManifestInvalidRevisionError(
'revision %s in %s not found' % (self.revisionExpr,
self.name))
def Sync_LocalHalf(self, syncbuf): def Sync_LocalHalf(self, syncbuf):
"""Perform only the local IO portion of the sync process. """Perform only the local IO portion of the sync process.
Network access is not required. Network access is not required.
@ -610,19 +639,7 @@ class Project(object):
all = self.bare_ref.all all = self.bare_ref.all
self.CleanPublishedCache(all) self.CleanPublishedCache(all)
rem = self.GetRemote(self.remote.name) revid = self.GetRevisionId(all)
rev = rem.ToLocal(self.revision)
if rev in all:
revid = all[rev]
elif IsId(rev):
revid = rev
else:
try:
revid = self.bare_git.rev_parse('--verify', '%s^0' % rev)
except GitError:
raise ManifestInvalidRevisionError(
'revision %s in %s not found' % (self.revision, self.name))
head = self.work_git.GetHead() head = self.work_git.GetHead()
if head.startswith(R_HEADS): if head.startswith(R_HEADS):
branch = head[len(R_HEADS):] branch = head[len(R_HEADS):]
@ -646,11 +663,11 @@ class Project(object):
# #
return return
lost = self._revlist(not_rev(rev), HEAD) lost = self._revlist(not_rev(revid), HEAD)
if lost: if lost:
syncbuf.info(self, "discarding %d commits", len(lost)) syncbuf.info(self, "discarding %d commits", len(lost))
try: try:
self._Checkout(rev, quiet=True) self._Checkout(revid, quiet=True)
except GitError, e: except GitError, e:
syncbuf.fail(self, e) syncbuf.fail(self, e)
return return
@ -663,9 +680,8 @@ class Project(object):
return return
branch = self.GetBranch(branch) branch = self.GetBranch(branch)
merge = branch.LocalMerge
if not merge: if not branch.LocalMerge:
# The current branch has no tracking configuration. # The current branch has no tracking configuration.
# Jump off it to a deatched HEAD. # Jump off it to a deatched HEAD.
# #
@ -673,17 +689,17 @@ class Project(object):
"leaving %s; does not track upstream", "leaving %s; does not track upstream",
branch.name) branch.name)
try: try:
self._Checkout(rev, quiet=True) self._Checkout(revid, quiet=True)
except GitError, e: except GitError, e:
syncbuf.fail(self, e) syncbuf.fail(self, e)
return return
self._CopyFiles() self._CopyFiles()
return return
upstream_gain = self._revlist(not_rev(HEAD), rev) upstream_gain = self._revlist(not_rev(HEAD), revid)
pub = self.WasPublished(branch.name, all) pub = self.WasPublished(branch.name, all)
if pub: if pub:
not_merged = self._revlist(not_rev(rev), pub) not_merged = self._revlist(not_rev(revid), pub)
if not_merged: if not_merged:
if upstream_gain: if upstream_gain:
# The user has published this branch and some of those # The user has published this branch and some of those
@ -700,69 +716,70 @@ class Project(object):
# strict subset. We can fast-forward safely. # strict subset. We can fast-forward safely.
# #
def _doff(): def _doff():
self._FastForward(rev) self._FastForward(revid)
self._CopyFiles() self._CopyFiles()
syncbuf.later1(self, _doff) syncbuf.later1(self, _doff)
return return
if merge == rev: # Examine the local commits not in the remote. Find the
try: # last one attributed to this user, if any.
old_merge = self.bare_git.rev_parse('%s@{1}' % merge) #
except GitError: local_changes = self._revlist(not_rev(revid), HEAD, format='%H %ce')
old_merge = merge last_mine = None
if old_merge == '0000000000000000000000000000000000000000' \ cnt_mine = 0
or old_merge == '': for commit in local_changes:
old_merge = merge commit_id, committer_email = commit.split(' ', 2)
else: if committer_email == self.UserEmail:
# The upstream switched on us. Time to cross our fingers last_mine = commit_id
# and pray that the old upstream also wasn't in the habit cnt_mine += 1
# of rebasing itself.
#
syncbuf.info(self, "manifest switched %s...%s", merge, rev)
old_merge = merge
if rev == old_merge: if not upstream_gain and cnt_mine == len(local_changes):
upstream_lost = []
else:
upstream_lost = self._revlist(not_rev(rev), old_merge)
if not upstream_lost and not upstream_gain:
# Trivially no changes caused by the upstream.
#
return return
if self.IsDirty(consider_untracked=False): if self.IsDirty(consider_untracked=False):
syncbuf.fail(self, _DirtyError()) syncbuf.fail(self, _DirtyError())
return return
if upstream_lost: # If the upstream switched on us, warn the user.
#
if branch.merge != self.revisionExpr:
if branch.merge and self.revisionExpr:
syncbuf.info(self,
'manifest switched %s...%s',
branch.merge,
self.revisionExpr)
elif branch.merge:
syncbuf.info(self,
'manifest no longer tracks %s',
branch.merge)
if cnt_mine < len(local_changes):
# Upstream rebased. Not everything in HEAD # Upstream rebased. Not everything in HEAD
# may have been caused by the user. # was created by this user.
# #
syncbuf.info(self, syncbuf.info(self,
"discarding %d commits removed from upstream", "discarding %d commits removed from upstream",
len(upstream_lost)) len(local_changes) - cnt_mine)
branch.remote = rem branch.remote = self.GetRemote(self.remote.name)
branch.merge = self.revision branch.merge = self.revisionExpr
branch.Save() branch.Save()
my_changes = self._revlist(not_rev(old_merge), HEAD) if cnt_mine > 0:
if my_changes:
def _dorebase(): def _dorebase():
self._Rebase(upstream = old_merge, onto = rev) self._Rebase(upstream = '%s^1' % last_mine, onto = revid)
self._CopyFiles() self._CopyFiles()
syncbuf.later2(self, _dorebase) syncbuf.later2(self, _dorebase)
elif upstream_lost: elif local_changes:
try: try:
self._ResetHard(rev) self._ResetHard(revid)
self._CopyFiles() self._CopyFiles()
except GitError, e: except GitError, e:
syncbuf.fail(self, e) syncbuf.fail(self, e)
return return
else: else:
def _doff(): def _doff():
self._FastForward(rev) self._FastForward(revid)
self._CopyFiles() self._CopyFiles()
syncbuf.later1(self, _doff) syncbuf.later1(self, _doff)
@ -784,7 +801,7 @@ class Project(object):
if GitCommand(self, cmd, bare=True).Wait() != 0: if GitCommand(self, cmd, bare=True).Wait() != 0:
return None return None
return DownloadedChange(self, return DownloadedChange(self,
remote.ToLocal(self.revision), self.GetRevisionId(),
change_id, change_id,
patch_id, patch_id,
self.bare_git.rev_parse('FETCH_HEAD')) self.bare_git.rev_parse('FETCH_HEAD'))
@ -808,15 +825,8 @@ class Project(object):
branch = self.GetBranch(name) branch = self.GetBranch(name)
branch.remote = self.GetRemote(self.remote.name) branch.remote = self.GetRemote(self.remote.name)
branch.merge = self.revision branch.merge = self.revisionExpr
revid = self.GetRevisionId(all)
rev = branch.LocalMerge
if rev in all:
revid = all[rev]
elif IsId(rev):
revid = rev
else:
revid = None
if head.startswith(R_HEADS): if head.startswith(R_HEADS):
try: try:
@ -837,7 +847,7 @@ class Project(object):
return True return True
if GitCommand(self, if GitCommand(self,
['checkout', '-b', branch.name, rev], ['checkout', '-b', branch.name, revid],
capture_stdout = True, capture_stdout = True,
capture_stderr = True).Wait() == 0: capture_stderr = True).Wait() == 0:
branch.Save() branch.Save()
@ -898,19 +908,12 @@ class Project(object):
# #
head = all[head] head = all[head]
rev = self.GetRemote(self.remote.name).ToLocal(self.revision) revid = self.GetRevisionId(all)
if rev in all: if head == revid:
revid = all[rev]
elif IsId(rev):
revid = rev
else:
revid = None
if revid and head == revid:
_lwrite(os.path.join(self.worktree, '.git', HEAD), _lwrite(os.path.join(self.worktree, '.git', HEAD),
'%s\n' % revid) '%s\n' % revid)
else: else:
self._Checkout(rev, quiet=True) self._Checkout(revid, quiet=True)
return GitCommand(self, return GitCommand(self,
['branch', '-D', name], ['branch', '-D', name],
@ -929,7 +932,7 @@ class Project(object):
if cb is None or name != cb: if cb is None or name != cb:
kill.append(name) kill.append(name)
rev = self.GetRemote(self.remote.name).ToLocal(self.revision) rev = self.GetRevisionId(left)
if cb is not None \ if cb is not None \
and not self._revlist(HEAD + '...' + rev) \ and not self._revlist(HEAD + '...' + rev) \
and not self.IsDirty(consider_untracked = False): and not self.IsDirty(consider_untracked = False):
@ -1065,17 +1068,11 @@ class Project(object):
raise raise
def _InitRemote(self): def _InitRemote(self):
if self.remote.fetchUrl: if self.remote.url:
remote = self.GetRemote(self.remote.name) remote = self.GetRemote(self.remote.name)
remote.url = self.remote.url
url = self.remote.fetchUrl remote.review = self.remote.review
while url.endswith('/'): remote.projectname = self.name
url = url[:-1]
url += '/%s.git' % self.name
remote.url = url
remote.review = self.remote.reviewUrl
if remote.projectname is None:
remote.projectname = self.name
if self.worktree: if self.worktree:
remote.ResetFetch(mirror=False) remote.ResetFetch(mirror=False)
@ -1083,37 +1080,27 @@ class Project(object):
remote.ResetFetch(mirror=True) remote.ResetFetch(mirror=True)
remote.Save() remote.Save()
for r in self.extraRemotes.values():
remote = self.GetRemote(r.name)
remote.url = r.fetchUrl
remote.review = r.reviewUrl
if r.projectName:
remote.projectname = r.projectName
elif remote.projectname is None:
remote.projectname = self.name
remote.ResetFetch()
remote.Save()
def _InitMRef(self): def _InitMRef(self):
if self.manifest.branch: if self.manifest.branch:
msg = 'manifest set to %s' % self.revision self._InitAnyMRef(R_M + self.manifest.branch)
ref = R_M + self.manifest.branch
cur = self.bare_ref.symref(ref)
if IsId(self.revision):
if cur != '' or self.bare_ref.get(ref) != self.revision:
dst = self.revision + '^0'
self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
else:
remote = self.GetRemote(self.remote.name)
dst = remote.ToLocal(self.revision)
if cur != dst:
self.bare_git.symbolic_ref('-m', msg, ref, dst)
def _InitMirrorHead(self): def _InitMirrorHead(self):
dst = self.GetRemote(self.remote.name).ToLocal(self.revision) self._InitAnyMRef(HEAD)
msg = 'manifest set to %s' % self.revision
self.bare_git.SetHead(dst, message=msg) def _InitAnyMRef(self, ref):
cur = self.bare_ref.symref(ref)
if self.revisionId:
if cur != '' or self.bare_ref.get(ref) != self.revisionId:
msg = 'manifest set to %s' % self.revisionId
dst = self.revisionId + '^0'
self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
else:
remote = self.GetRemote(self.remote.name)
dst = remote.ToLocal(self.revisionExpr)
if cur != dst:
msg = 'manifest set to %s' % self.revisionExpr
self.bare_git.symbolic_ref('-m', msg, ref, dst)
def _InitWorkTree(self): def _InitWorkTree(self):
dotgit = os.path.join(self.worktree, '.git') dotgit = os.path.join(self.worktree, '.git')
@ -1140,14 +1127,11 @@ class Project(object):
else: else:
raise raise
rev = self.GetRemote(self.remote.name).ToLocal(self.revision) _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
rev = self.bare_git.rev_parse('%s^0' % rev)
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % rev)
cmd = ['read-tree', '--reset', '-u'] cmd = ['read-tree', '--reset', '-u']
cmd.append('-v') cmd.append('-v')
cmd.append('HEAD') cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0: if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree") raise GitError("cannot initialize work tree")
self._CopyFiles() self._CopyFiles()
@ -1155,11 +1139,11 @@ class Project(object):
def _gitdir_path(self, path): def _gitdir_path(self, path):
return os.path.join(self.gitdir, path) return os.path.join(self.gitdir, path)
def _revlist(self, *args): def _revlist(self, *args, **kw):
cmd = [] a = []
cmd.extend(args) a.extend(args)
cmd.append('--') a.append('--')
return self.work_git.rev_list(*args) return self.work_git.rev_list(*a, **kw)
@property @property
def _allrefs(self): def _allrefs(self):
@ -1284,8 +1268,11 @@ class Project(object):
self.update_ref('-d', name, old) self.update_ref('-d', name, old)
self._project.bare_ref.deleted(name) self._project.bare_ref.deleted(name)
def rev_list(self, *args): def rev_list(self, *args, **kw):
cmdv = ['rev-list'] if 'format' in kw:
cmdv = ['log', '--pretty=format:%s' % kw['format']]
else:
cmdv = ['rev-list']
cmdv.extend(args) cmdv.extend(args)
p = GitCommand(self._project, p = GitCommand(self._project,
cmdv, cmdv,
@ -1294,7 +1281,9 @@ class Project(object):
capture_stderr = True) capture_stderr = True)
r = [] r = []
for line in p.process.stdout: for line in p.process.stdout:
r.append(line[:-1]) if line[-1] == '\n':
line = line[:-1]
r.append(line)
if p.Wait() != 0: if p.Wait() != 0:
raise GitError('%s rev-list %s: %s' % ( raise GitError('%s rev-list %s: %s' % (
self._project.name, self._project.name,
@ -1441,9 +1430,10 @@ class MetaProject(Project):
name = name, name = name,
gitdir = gitdir, gitdir = gitdir,
worktree = worktree, worktree = worktree,
remote = Remote('origin'), remote = RemoteSpec('origin'),
relpath = '.repo/%s' % name, relpath = '.repo/%s' % name,
revision = 'refs/heads/master') revisionExpr = 'refs/heads/master',
revisionId = None)
def PreSync(self): def PreSync(self):
if self.Exists: if self.Exists:
@ -1451,7 +1441,8 @@ class MetaProject(Project):
if cb: if cb:
base = self.GetBranch(cb).merge base = self.GetBranch(cb).merge
if base: if base:
self.revision = base self.revisionExpr = base
self.revisionId = None
@property @property
def LastFetch(self): def LastFetch(self):
@ -1465,16 +1456,11 @@ class MetaProject(Project):
def HasChanges(self): def HasChanges(self):
"""Has the remote received new commits not yet checked out? """Has the remote received new commits not yet checked out?
""" """
if not self.remote or not self.revision: if not self.remote or not self.revisionExpr:
return False return False
all = self.bare_ref.all all = self.bare_ref.all
rev = self.GetRemote(self.remote.name).ToLocal(self.revision) revid = self.GetRevisionId(all)
if rev in all:
revid = all[rev]
else:
revid = rev
head = self.work_git.GetHead() head = self.work_git.GetHead()
if head.startswith(R_HEADS): if head.startswith(R_HEADS):
try: try:
@ -1484,6 +1470,6 @@ class MetaProject(Project):
if revid == head: if revid == head:
return False return False
elif self._revlist(not_rev(HEAD), rev): elif self._revlist(not_rev(HEAD), revid):
return True return True
return False return False

View File

@ -1,25 +0,0 @@
#
# Copyright (C) 2008 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class Remote(object):
def __init__(self, name,
fetch=None,
review=None,
projectName=None):
self.name = name
self.fetchUrl = fetch
self.reviewUrl = review
self.projectName = projectName
self.requiredCommits = []

2
repo
View File

@ -505,7 +505,7 @@ def _RunSelf(wrapper_path):
my_git = os.path.join(my_dir, '.git') my_git = os.path.join(my_dir, '.git')
if os.path.isfile(my_main) and os.path.isdir(my_git): if os.path.isfile(my_main) and os.path.isdir(my_git):
for name in ['manifest.py', for name in ['git_config.py',
'project.py', 'project.py',
'subcmds']: 'subcmds']:
if not os.path.exists(os.path.join(my_dir, name)): if not os.path.exists(os.path.join(my_dir, name)):

View File

@ -61,12 +61,34 @@ class Branches(Command):
%prog [<project>...] %prog [<project>...]
Summarizes the currently available topic branches. Summarizes the currently available topic branches.
"""
def _Options(self, p): Branch Display
p.add_option('-a', '--all', --------------
dest='all', action='store_true',
help='show all branches, not just the majority') The branch display output by this command is organized into four
columns of information; for example:
*P nocolor | in repo
repo2 |
The first column contains a * if the branch is the currently
checked out branch in any of the specified projects, or a blank
if no project has the branch checked out.
The second column contains either blank, p or P, depending upon
the upload status of the branch.
(blank): branch not yet published by repo upload
P: all commits were published by repo upload
p: only some commits were published by repo upload
The third column contains the branch name.
The fourth column (after the | separator) lists the projects that
the branch appears in, or does not appear in. If no project list
is shown, then the branch appears in all projects.
"""
def Execute(self, opt, args): def Execute(self, opt, args):
projects = self.GetProjects(args) projects = self.GetProjects(args)
@ -84,18 +106,6 @@ Summarizes the currently available topic branches.
names = all.keys() names = all.keys()
names.sort() names.sort()
if not opt.all and not args:
# No -a and no specific projects listed; try to filter the
# results down to only the majority of projects.
#
n = []
for name in names:
i = all[name]
if i.IsCurrent \
or 80 <= (100 * len(i.projects)) / project_cnt:
n.append(name)
names = n
if not names: if not names:
print >>sys.stderr, ' (no branches)' print >>sys.stderr, ' (no branches)'
return return
@ -126,7 +136,7 @@ Summarizes the currently available topic branches.
hdr('%c%c %-*s' % (current, published, width, name)) hdr('%c%c %-*s' % (current, published, width, name))
out.write(' |') out.write(' |')
if in_cnt < project_cnt and (in_cnt == 1 or opt.all): if in_cnt < project_cnt and (in_cnt == 1):
fmt = out.write fmt = out.write
paths = [] paths = []
if in_cnt < project_cnt - in_cnt: if in_cnt < project_cnt - in_cnt:

View File

@ -160,10 +160,8 @@ terminal and are not redirected.
setenv('REPO_PROJECT', project.name) setenv('REPO_PROJECT', project.name)
setenv('REPO_PATH', project.relpath) setenv('REPO_PATH', project.relpath)
setenv('REPO_REMOTE', project.remote.name) setenv('REPO_REMOTE', project.remote.name)
setenv('REPO_LREV', project\ setenv('REPO_LREV', project.GetRevisionId())
.GetRemote(project.remote.name)\ setenv('REPO_RREV', project.revisionExpr)
.ToLocal(project.revision))
setenv('REPO_RREV', project.revision)
if mirror: if mirror:
setenv('GIT_DIR', project.gitdir) setenv('GIT_DIR', project.gitdir)

View File

@ -17,7 +17,7 @@ import sys
from optparse import SUPPRESS_HELP from optparse import SUPPRESS_HELP
from color import Coloring from color import Coloring
from command import PagedCommand from command import PagedCommand
from git_command import GitCommand from git_command import git_require, GitCommand
class GrepColoring(Coloring): class GrepColoring(Coloring):
def __init__(self, config): def __init__(self, config):
@ -158,7 +158,7 @@ contain a line that matches both expressions:
out = GrepColoring(self.manifest.manifestProject.config) out = GrepColoring(self.manifest.manifestProject.config)
cmd_argv = ['grep'] cmd_argv = ['grep']
if out.is_on: if out.is_on and git_require((1,6,3)):
cmd_argv.append('--color') cmd_argv.append('--color')
cmd_argv.extend(getattr(opt,'cmd_argv',[])) cmd_argv.extend(getattr(opt,'cmd_argv',[]))

View File

@ -19,9 +19,8 @@ import sys
from color import Coloring from color import Coloring
from command import InteractiveCommand, MirrorSafeCommand from command import InteractiveCommand, MirrorSafeCommand
from error import ManifestParseError from error import ManifestParseError
from remote import Remote
from project import SyncBuffer from project import SyncBuffer
from git_command import git, MIN_GIT_VERSION from git_command import git_require, MIN_GIT_VERSION
class Init(InteractiveCommand, MirrorSafeCommand): class Init(InteractiveCommand, MirrorSafeCommand):
common = True common = True
@ -86,19 +85,6 @@ to update the working directory files.
dest='no_repo_verify', action='store_true', dest='no_repo_verify', action='store_true',
help='do not verify repo source code') help='do not verify repo source code')
def _CheckGitVersion(self):
ver_str = git.version()
if not ver_str.startswith('git version '):
print >>sys.stderr, 'error: "%s" unsupported' % ver_str
sys.exit(1)
ver_str = ver_str[len('git version '):].strip()
ver_act = tuple(map(lambda x: int(x), ver_str.split('.')[0:3]))
if ver_act < MIN_GIT_VERSION:
need = '.'.join(map(lambda x: str(x), MIN_GIT_VERSION))
print >>sys.stderr, 'fatal: git %s or later required' % need
sys.exit(1)
def _SyncManifest(self, opt): def _SyncManifest(self, opt):
m = self.manifest.manifestProject m = self.manifest.manifestProject
is_new = not m.Exists is_new = not m.Exists
@ -114,12 +100,12 @@ to update the working directory files.
m._InitGitDir() m._InitGitDir()
if opt.manifest_branch: if opt.manifest_branch:
m.revision = opt.manifest_branch m.revisionExpr = opt.manifest_branch
else: else:
m.revision = 'refs/heads/master' m.revisionExpr = 'refs/heads/master'
else: else:
if opt.manifest_branch: if opt.manifest_branch:
m.revision = opt.manifest_branch m.revisionExpr = opt.manifest_branch
else: else:
m.PreSync() m.PreSync()
@ -162,20 +148,33 @@ to update the working directory files.
print >>sys.stderr, 'fatal: %s' % str(e) print >>sys.stderr, 'fatal: %s' % str(e)
sys.exit(1) sys.exit(1)
def _PromptKey(self, prompt, key, value): def _Prompt(self, prompt, value):
mp = self.manifest.manifestProject mp = self.manifest.manifestProject
sys.stdout.write('%-10s [%s]: ' % (prompt, value)) sys.stdout.write('%-10s [%s]: ' % (prompt, value))
a = sys.stdin.readline().strip() a = sys.stdin.readline().strip()
if a != '' and a != value: if a == '':
mp.config.SetString(key, a) return value
return a
def _ConfigureUser(self): def _ConfigureUser(self):
mp = self.manifest.manifestProject mp = self.manifest.manifestProject
print '' while True:
self._PromptKey('Your Name', 'user.name', mp.UserName) print ''
self._PromptKey('Your Email', 'user.email', mp.UserEmail) name = self._Prompt('Your Name', mp.UserName)
email = self._Prompt('Your Email', mp.UserEmail)
print ''
print 'Your identity is: %s <%s>' % (name, email)
sys.stdout.write('is this correct [yes/no]? ')
if 'yes' == sys.stdin.readline().strip():
break
if name != mp.UserName:
mp.config.SetString('user.name', name)
if email != mp.UserEmail:
mp.config.SetString('user.email', email)
def _HasColorSet(self, gc): def _HasColorSet(self, gc):
for n in ['ui', 'diff', 'status']: for n in ['ui', 'diff', 'status']:
@ -215,7 +214,7 @@ to update the working directory files.
gc.SetString('color.ui', 'auto') gc.SetString('color.ui', 'auto')
def Execute(self, opt, args): def Execute(self, opt, args):
self._CheckGitVersion() git_require(MIN_GIT_VERSION, fail=True)
self._SyncManifest(opt) self._SyncManifest(opt)
self._LinkManifest(opt.manifest_name) self._LinkManifest(opt.manifest_name)

View File

@ -16,12 +16,15 @@
from optparse import SUPPRESS_HELP from optparse import SUPPRESS_HELP
import os import os
import re import re
import shutil
import subprocess import subprocess
import sys import sys
import time import time
from git_command import GIT from git_command import GIT
from project import HEAD from project import HEAD
from project import Project
from project import RemoteSpec
from command import Command, MirrorSafeCommand from command import Command, MirrorSafeCommand
from error import RepoChangedException, GitError from error import RepoChangedException, GitError
from project import R_HEADS from project import R_HEADS
@ -117,6 +120,61 @@ later is required to fix a server side protocol bug.
pm.end() pm.end()
return fetched return fetched
def UpdateProjectList(self):
new_project_paths = []
for project in self.manifest.projects.values():
if project.relpath:
new_project_paths.append(project.relpath)
file_name = 'project.list'
file_path = os.path.join(self.manifest.repodir, file_name)
old_project_paths = []
if os.path.exists(file_path):
fd = open(file_path, 'r')
try:
old_project_paths = fd.read().split('\n')
finally:
fd.close()
for path in old_project_paths:
if not path:
continue
if path not in new_project_paths:
project = Project(
manifest = self.manifest,
name = path,
remote = RemoteSpec('origin'),
gitdir = os.path.join(self.manifest.topdir,
path, '.git'),
worktree = os.path.join(self.manifest.topdir, path),
relpath = path,
revisionExpr = 'HEAD',
revisionId = None)
if project.IsDirty():
print >>sys.stderr, 'error: Cannot remove project "%s": \
uncommitted changes are present' % project.relpath
print >>sys.stderr, ' commit changes, then run sync again'
return -1
else:
print >>sys.stderr, 'Deleting obsolete path %s' % project.worktree
shutil.rmtree(project.worktree)
# Try deleting parent subdirs if they are empty
dir = os.path.dirname(project.worktree)
while dir != self.manifest.topdir:
try:
os.rmdir(dir)
except OSError:
break
dir = os.path.dirname(dir)
new_project_paths.sort()
fd = open(file_path, 'w')
try:
fd.write('\n'.join(new_project_paths))
fd.write('\n')
finally:
fd.close()
return 0
def Execute(self, opt, args): def Execute(self, opt, args):
if opt.network_only and opt.detach_head: if opt.network_only and opt.detach_head:
print >>sys.stderr, 'error: cannot combine -n and -d' print >>sys.stderr, 'error: cannot combine -n and -d'
@ -164,6 +222,13 @@ later is required to fix a server side protocol bug.
missing.append(project) missing.append(project)
self._Fetch(missing) self._Fetch(missing)
if self.manifest.IsMirror:
# bail out now, we have no working tree
return
if self.UpdateProjectList():
sys.exit(1)
syncbuf = SyncBuffer(mp.config, syncbuf = SyncBuffer(mp.config,
detach_head = opt.detach_head) detach_head = opt.detach_head)
pm = Progress('Syncing work tree', len(all)) pm = Progress('Syncing work tree', len(all))
@ -207,17 +272,14 @@ def _VerifyTag(project):
warning: Cannot automatically authenticate repo.""" warning: Cannot automatically authenticate repo."""
return True return True
remote = project.GetRemote(project.remote.name)
ref = remote.ToLocal(project.revision)
try: try:
cur = project.bare_git.describe(ref) cur = project.bare_git.describe(project.GetRevisionId())
except GitError: except GitError:
cur = None cur = None
if not cur \ if not cur \
or re.compile(r'^.*-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur): or re.compile(r'^.*-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur):
rev = project.revision rev = project.revisionExpr
if rev.startswith(R_HEADS): if rev.startswith(R_HEADS):
rev = rev[len(R_HEADS):] rev = rev[len(R_HEADS):]

3
tests/fixtures/test.gitconfig vendored Normal file
View File

@ -0,0 +1,3 @@
[section]
empty
nonempty = true

43
tests/test_git_config.py Normal file
View File

@ -0,0 +1,43 @@
import os
import unittest
import git_config
def fixture(*paths):
"""Return a path relative to test/fixtures.
"""
return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
class GitConfigUnitTest(unittest.TestCase):
"""Tests the GitConfig class.
"""
def setUp(self):
"""Create a GitConfig object using the test.gitconfig fixture.
"""
config_fixture = fixture('test.gitconfig')
self.config = git_config.GitConfig(config_fixture)
def test_GetString_with_empty_config_values(self):
"""
Test config entries with no value.
[section]
empty
"""
val = self.config.GetString('section.empty')
self.assertEqual(val, None)
def test_GetString_with_true_value(self):
"""
Test config entries with a string value.
[section]
nonempty = true
"""
val = self.config.GetString('section.nonempty')
self.assertEqual(val, 'true')
if __name__ == '__main__':
unittest.main()