Compare commits

...

37 Commits

Author SHA1 Message Date
9bb1816bdc Fixing project renaming bug.
This bug happens when a project gets added to the manifest, and
then is renamed. Users who happened to have run "repo sync" after
the project was added but before the rename happened will try to
read the data from the old project, as the manifest was only updated
after all projects were updated successfully.
2009-12-10 15:24:45 -08:00
c24c720b61 Fix error parsing a non-existant configuration file
If a file (e.g. ~/.gitconfig) does not exist, we get None
here rather than a string.  NoneType lacks rstrip() so we
cannot strip it.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-07-02 16:12:57 -07:00
2d1a396897 Document how to contribute to the repo project
Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-07-02 13:18:55 -07:00
1dcb58a7d0 Support GIT_EDITOR='vim -c "set textwidth=80"'
If there are shell special characters in the editor string, we must
use /bin/sh to parse and execute it, rather than trying to rely on
a simple split(' ').  This avoids vim starting up with two empty
buffers, due to a misparsed command line.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-07-02 12:45:47 -07:00
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
bc7ef67d9b Automatically guess Gerrit change number in "repo upload --replace"
This feature only works if you have one commit to replace right now
(the common case).
2009-05-05 15:01:18 -07:00
2f968c943b Fix ssh://user@hostname/ style URLs parsing
I only tested this with ssh://hostname/ style URLs, so I failed
to test ssh://user@hostname/ format, which failed if the hostname
portion was longer than 1 character.

Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-04-30 14:30:28 -07:00
2b5b4ac292 Disable SSH ControlMaster option on Cygwin
Bug: REPO-29
Signed-off-by: Shawn O. Pearce <sop@google.com>
2009-04-23 17:22:18 -07:00
6f6cd77a50 Require a project or '--all' to be specified when using 'repo start'. 2009-04-22 18:05:50 -07:00
23 changed files with 612 additions and 362 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.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>

80
SUBMITTING_PATCHES Normal file
View File

@ -0,0 +1,80 @@
Short Version:
- Make small logical changes.
- Provide a meaningful commit message.
- Make sure all code is under the Apache License, 2.0.
- Publish your changes for review:
git push ssh://review.source.android.com:29418/tools/repo.git HEAD:refs/for/master
Long Version:
I wanted a file describing how to submit patches for repo,
so I started with the one found in the core Git distribution
(Documentation/SubmittingPatches), which itself was based on the
patch submission guidelines for the Linux kernel.
However there are some differences, so please review and familiarize
yourself with the following relevant bits:
(1) Make separate commits for logically separate changes.
Unless your patch is really trivial, you should not be sending
out a patch that was generated between your working tree and your
commit head. Instead, always make a commit with complete commit
message and generate a series of patches from your repository.
It is a good discipline.
Describe the technical detail of the change(s).
If your description starts to get too long, that's a sign that you
probably need to split up your commit to finer grained pieces.
(2) Check the license
repo is licensed under the Apache License, 2.0.
Because of this licensing model *every* file within the project
*must* list the license that covers it in the header of the file.
Any new contributions to an existing file *must* be submitted under
the current license of that file. Any new files *must* clearly
indicate which license they are provided under in the file header.
Please verify that you are legally allowed and willing to submit your
changes under the license covering each file *prior* to submitting
your patch. It is virtually impossible to remove a patch once it
has been applied and pushed out.
(3) Sending your patches.
Do not email your patches to anyone.
Instead, login to the Gerrit Code Review tool at:
https://review.source.android.com/
Ensure you have completed one of the necessary contributor
agreements, providing documentation to the project maintainers that
they have right to redistribute your work under the Apache License:
https://review.source.android.com/#settings,agreements
Ensure you have registered one or more SSH public keys, so you can
push your commits directly over SSH:
https://review.source.android.com/#settings,ssh-keys
Push your patches over SSH to the review server, possibly through
a remembered remote to make this easier in the future:
git config remote.review.url ssh://review.source.android.com:29418/tools/repo.git
git config remote.review.push HEAD:refs/for/master
git push review
You will be automatically emailed a copy of your commits, and any
comments made by the project maintainers.

View File

@ -23,32 +23,23 @@ following DTD:
<!ELEMENT manifest (remote*,
default?,
remove-project*,
project*,
add-remote*)>
project*)>
<!ELEMENT remote (EMPTY)>
<!ATTLIST remote name ID #REQUIRED>
<!ATTLIST remote fetch CDATA #REQUIRED>
<!ATTLIST remote review CDATA #IMPLIED>
<!ATTLIST remote project-name CDATA #IMPLIED>
<!ELEMENT default (EMPTY)>
<!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED>
<!ELEMENT project (remote*)>
<!ELEMENT project (EMPTY)>
<!ATTLIST project name CDATA #REQUIRED>
<!ATTLIST project path CDATA #IMPLIED>
<!ATTLIST project remote IDREF #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)>
<!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;
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
---------------
@ -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
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
----------------------

View File

@ -14,6 +14,7 @@
# limitations under the License.
import os
import re
import sys
import subprocess
import tempfile
@ -38,9 +39,10 @@ class Editor(object):
if e:
return e
e = cls.globalConfig.GetString('core.editor')
if e:
return e
if cls.globalConfig:
e = cls.globalConfig.GetString('core.editor')
if e:
return e
e = os.getenv('VISUAL')
if e:
@ -69,15 +71,33 @@ least one of these before using this command."""
Returns:
new value of edited text; None if editing did not succeed
"""
editor = cls._GetEditor().split()
editor = cls._GetEditor()
if editor == ':':
return data
fd, path = tempfile.mkstemp()
try:
os.write(fd, data)
os.close(fd)
fd = None
if subprocess.Popen(editor + [path]).wait() != 0:
raise EditorError()
if re.compile("^.*[$ \t'].*$").match(editor):
args = [editor + ' "$@"']
shell = True
else:
args = [editor]
shell = False
args.append(path)
try:
rc = subprocess.Popen(args, shell=shell).wait()
except OSError, e:
raise EditorError('editor failed, %s: %s %s'
% (str(e), editor, path))
if rc != 0:
raise EditorError('editor failed with exit status %d: %s %s'
% (rc, editor, path))
fd2 = open(path)
try:
return fd2.read()

View File

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

View File

@ -68,6 +68,30 @@ class _GitCall(object):
return fun
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):
def __init__(self,
project,

View File

@ -56,16 +56,20 @@ class GitConfig(object):
return cls(file = os.path.join(gitdir, 'config'),
defaults = defaults)
def __init__(self, file, defaults=None):
def __init__(self, file, defaults=None, pickleFile=None):
self.file = file
self.defaults = defaults
self._cache_dict = None
self._section_dict = None
self._remotes = {}
self._branches = {}
self._pickle = os.path.join(
os.path.dirname(self.file),
'.repopickle_' + os.path.basename(self.file))
if pickleFile is None:
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):
"""Return true if this configuration file has the key.
@ -172,6 +176,11 @@ class GitConfig(object):
self._branches[b.name] = b
return b
def GetSubSections(self, section):
"""List all subsection names matching $section.*.*
"""
return self._sections.get(section, set())
def HasSection(self, section, subsection = ''):
"""Does at least one key in section.subsection exist?
"""
@ -227,6 +236,9 @@ class GitConfig(object):
return cPickle.load(fd)
finally:
fd.close()
except EOFError:
os.remove(self._pickle)
return None
except IOError:
os.remove(self._pickle)
return None
@ -247,21 +259,28 @@ class GitConfig(object):
os.remove(self._pickle)
def _ReadGit(self):
d = self._do('--null', '--list')
c = {}
while d:
lf = d.index('\n')
nul = d.index('\0', lf + 1)
"""
Read configuration data from git.
key = _key(d[0:lf])
val = d[lf + 1:nul]
This internal method populates the GitConfig cache.
"""
c = {}
d = self._do('--null', '--list')
if d is None:
return c
for line in d.rstrip('\0').split('\0'):
if '\n' in line:
key, val = line.split('\n', 1)
else:
key = line
val = None
if key in c:
c[key].append(val)
else:
c[key] = [val]
d = d[nul + 1:]
return c
def _do(self, *args):
@ -346,7 +365,7 @@ def _open_ssh(host, port):
if not _ssh_master \
or 'GIT_SSH' in os.environ \
or sys.platform == 'win32':
or sys.platform in ('win32', 'cygwin'):
# failed earlier, or cygwin ssh can't do this
#
return False
@ -373,8 +392,11 @@ def _open_ssh(host, port):
def close_ssh():
for key,p in _ssh_cache.iteritems():
os.kill(p.pid, SIGTERM)
p.wait()
try:
os.kill(p.pid, SIGTERM)
p.wait()
except OSError:
pass
_ssh_cache.clear()
d = _ssh_sock(create=False)
@ -385,7 +407,7 @@ def close_ssh():
pass
URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/])/')
URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
def _preconnect(url):
m = URI_ALL.match(url)
@ -405,6 +427,7 @@ def _preconnect(url):
host = m.group(1)
return _open_ssh(host, 22)
return False
class Remote(object):
"""Configuration options related to a remote.

View File

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

View File

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

View File

@ -26,7 +26,6 @@ from git_command import GitCommand
from git_config import GitConfig, IsId
from error import GitError, ImportError, UploadError
from error import ManifestInvalidRevisionError
from remote import Remote
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
@ -155,6 +154,19 @@ class ReviewableBranch(object):
self.replace_changes,
people)
def GetPublishedRefs(self):
refs = {}
output = self.project.bare_git.ls_remote(
self.branch.remote.SshReviewUrl(self.project.UserEmail),
'refs/changes/*')
for line in output.split('\n'):
try:
(sha, ref) = line.split()
refs[sha] = ref
except ValueError:
pass
return refs
class StatusColoring(Coloring):
def __init__(self, config):
@ -199,6 +211,14 @@ class _CopyFile:
except IOError:
_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):
def __init__(self,
@ -208,16 +228,24 @@ class Project(object):
gitdir,
worktree,
relpath,
revision):
revisionExpr,
revisionId):
self.manifest = manifest
self.name = name
self.remote = remote
self.gitdir = gitdir
self.worktree = worktree
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.extraRemotes = {}
self.copyfiles = []
self.config = GitConfig.ForRepository(
gitdir = self.gitdir,
@ -566,9 +594,6 @@ class Project(object):
self._InitGitDir()
self._InitRemote()
for r in self.extraRemotes.values():
if not self._RemoteFetch(r.name):
return False
if not self._RemoteFetch():
return False
@ -589,6 +614,23 @@ class Project(object):
for file in self.copyfiles:
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):
"""Perform only the local IO portion of the sync process.
Network access is not required.
@ -597,19 +639,7 @@ class Project(object):
all = self.bare_ref.all
self.CleanPublishedCache(all)
rem = self.GetRemote(self.remote.name)
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))
revid = self.GetRevisionId(all)
head = self.work_git.GetHead()
if head.startswith(R_HEADS):
branch = head[len(R_HEADS):]
@ -633,11 +663,11 @@ class Project(object):
#
return
lost = self._revlist(not_rev(rev), HEAD)
lost = self._revlist(not_rev(revid), HEAD)
if lost:
syncbuf.info(self, "discarding %d commits", len(lost))
try:
self._Checkout(rev, quiet=True)
self._Checkout(revid, quiet=True)
except GitError, e:
syncbuf.fail(self, e)
return
@ -650,9 +680,8 @@ class Project(object):
return
branch = self.GetBranch(branch)
merge = branch.LocalMerge
if not merge:
if not branch.LocalMerge:
# The current branch has no tracking configuration.
# Jump off it to a deatched HEAD.
#
@ -660,17 +689,17 @@ class Project(object):
"leaving %s; does not track upstream",
branch.name)
try:
self._Checkout(rev, quiet=True)
self._Checkout(revid, quiet=True)
except GitError, e:
syncbuf.fail(self, e)
return
self._CopyFiles()
return
upstream_gain = self._revlist(not_rev(HEAD), rev)
upstream_gain = self._revlist(not_rev(HEAD), revid)
pub = self.WasPublished(branch.name, all)
if pub:
not_merged = self._revlist(not_rev(rev), pub)
not_merged = self._revlist(not_rev(revid), pub)
if not_merged:
if upstream_gain:
# The user has published this branch and some of those
@ -687,69 +716,70 @@ class Project(object):
# strict subset. We can fast-forward safely.
#
def _doff():
self._FastForward(rev)
self._FastForward(revid)
self._CopyFiles()
syncbuf.later1(self, _doff)
return
if merge == rev:
try:
old_merge = self.bare_git.rev_parse('%s@{1}' % merge)
except GitError:
old_merge = merge
if old_merge == '0000000000000000000000000000000000000000' \
or old_merge == '':
old_merge = merge
else:
# The upstream switched on us. Time to cross our fingers
# and pray that the old upstream also wasn't in the habit
# of rebasing itself.
#
syncbuf.info(self, "manifest switched %s...%s", merge, rev)
old_merge = merge
# Examine the local commits not in the remote. Find the
# last one attributed to this user, if any.
#
local_changes = self._revlist(not_rev(revid), HEAD, format='%H %ce')
last_mine = None
cnt_mine = 0
for commit in local_changes:
commit_id, committer_email = commit.split(' ', 2)
if committer_email == self.UserEmail:
last_mine = commit_id
cnt_mine += 1
if rev == old_merge:
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.
#
if not upstream_gain and cnt_mine == len(local_changes):
return
if self.IsDirty(consider_untracked=False):
syncbuf.fail(self, _DirtyError())
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
# may have been caused by the user.
# was created by this user.
#
syncbuf.info(self,
"discarding %d commits removed from upstream",
len(upstream_lost))
len(local_changes) - cnt_mine)
branch.remote = rem
branch.merge = self.revision
branch.remote = self.GetRemote(self.remote.name)
branch.merge = self.revisionExpr
branch.Save()
my_changes = self._revlist(not_rev(old_merge), HEAD)
if my_changes:
if cnt_mine > 0:
def _dorebase():
self._Rebase(upstream = old_merge, onto = rev)
self._Rebase(upstream = '%s^1' % last_mine, onto = revid)
self._CopyFiles()
syncbuf.later2(self, _dorebase)
elif upstream_lost:
elif local_changes:
try:
self._ResetHard(rev)
self._ResetHard(revid)
self._CopyFiles()
except GitError, e:
syncbuf.fail(self, e)
return
else:
def _doff():
self._FastForward(rev)
self._FastForward(revid)
self._CopyFiles()
syncbuf.later1(self, _doff)
@ -771,7 +801,7 @@ class Project(object):
if GitCommand(self, cmd, bare=True).Wait() != 0:
return None
return DownloadedChange(self,
remote.ToLocal(self.revision),
self.GetRevisionId(),
change_id,
patch_id,
self.bare_git.rev_parse('FETCH_HEAD'))
@ -795,15 +825,8 @@ class Project(object):
branch = self.GetBranch(name)
branch.remote = self.GetRemote(self.remote.name)
branch.merge = self.revision
rev = branch.LocalMerge
if rev in all:
revid = all[rev]
elif IsId(rev):
revid = rev
else:
revid = None
branch.merge = self.revisionExpr
revid = self.GetRevisionId(all)
if head.startswith(R_HEADS):
try:
@ -824,7 +847,7 @@ class Project(object):
return True
if GitCommand(self,
['checkout', '-b', branch.name, rev],
['checkout', '-b', branch.name, revid],
capture_stdout = True,
capture_stderr = True).Wait() == 0:
branch.Save()
@ -885,19 +908,12 @@ class Project(object):
#
head = all[head]
rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
if rev in all:
revid = all[rev]
elif IsId(rev):
revid = rev
else:
revid = None
if revid and head == revid:
revid = self.GetRevisionId(all)
if head == revid:
_lwrite(os.path.join(self.worktree, '.git', HEAD),
'%s\n' % revid)
else:
self._Checkout(rev, quiet=True)
self._Checkout(revid, quiet=True)
return GitCommand(self,
['branch', '-D', name],
@ -916,7 +932,7 @@ class Project(object):
if cb is None or name != cb:
kill.append(name)
rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
rev = self.GetRevisionId(left)
if cb is not None \
and not self._revlist(HEAD + '...' + rev) \
and not self.IsDirty(consider_untracked = False):
@ -1052,17 +1068,11 @@ class Project(object):
raise
def _InitRemote(self):
if self.remote.fetchUrl:
if self.remote.url:
remote = self.GetRemote(self.remote.name)
url = self.remote.fetchUrl
while url.endswith('/'):
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
remote.url = self.remote.url
remote.review = self.remote.review
remote.projectname = self.name
if self.worktree:
remote.ResetFetch(mirror=False)
@ -1070,37 +1080,27 @@ class Project(object):
remote.ResetFetch(mirror=True)
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):
if self.manifest.branch:
msg = 'manifest set to %s' % self.revision
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)
self._InitAnyMRef(R_M + self.manifest.branch)
def _InitMirrorHead(self):
dst = self.GetRemote(self.remote.name).ToLocal(self.revision)
msg = 'manifest set to %s' % self.revision
self.bare_git.SetHead(dst, message=msg)
self._InitAnyMRef(HEAD)
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):
dotgit = os.path.join(self.worktree, '.git')
@ -1127,14 +1127,11 @@ class Project(object):
else:
raise
rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
rev = self.bare_git.rev_parse('%s^0' % rev)
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % rev)
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
cmd = ['read-tree', '--reset', '-u']
cmd.append('-v')
cmd.append('HEAD')
cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree")
self._CopyFiles()
@ -1142,11 +1139,11 @@ class Project(object):
def _gitdir_path(self, path):
return os.path.join(self.gitdir, path)
def _revlist(self, *args):
cmd = []
cmd.extend(args)
cmd.append('--')
return self.work_git.rev_list(*args)
def _revlist(self, *args, **kw):
a = []
a.extend(args)
a.append('--')
return self.work_git.rev_list(*a, **kw)
@property
def _allrefs(self):
@ -1271,8 +1268,11 @@ class Project(object):
self.update_ref('-d', name, old)
self._project.bare_ref.deleted(name)
def rev_list(self, *args):
cmdv = ['rev-list']
def rev_list(self, *args, **kw):
if 'format' in kw:
cmdv = ['log', '--pretty=format:%s' % kw['format']]
else:
cmdv = ['rev-list']
cmdv.extend(args)
p = GitCommand(self._project,
cmdv,
@ -1281,7 +1281,9 @@ class Project(object):
capture_stderr = True)
r = []
for line in p.process.stdout:
r.append(line[:-1])
if line[-1] == '\n':
line = line[:-1]
r.append(line)
if p.Wait() != 0:
raise GitError('%s rev-list %s: %s' % (
self._project.name,
@ -1428,9 +1430,10 @@ class MetaProject(Project):
name = name,
gitdir = gitdir,
worktree = worktree,
remote = Remote('origin'),
remote = RemoteSpec('origin'),
relpath = '.repo/%s' % name,
revision = 'refs/heads/master')
revisionExpr = 'refs/heads/master',
revisionId = None)
def PreSync(self):
if self.Exists:
@ -1438,7 +1441,8 @@ class MetaProject(Project):
if cb:
base = self.GetBranch(cb).merge
if base:
self.revision = base
self.revisionExpr = base
self.revisionId = None
@property
def LastFetch(self):
@ -1452,16 +1456,11 @@ class MetaProject(Project):
def HasChanges(self):
"""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
all = self.bare_ref.all
rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
if rev in all:
revid = all[rev]
else:
revid = rev
revid = self.GetRevisionId(all)
head = self.work_git.GetHead()
if head.startswith(R_HEADS):
try:
@ -1471,6 +1470,6 @@ class MetaProject(Project):
if revid == head:
return False
elif self._revlist(not_rev(HEAD), rev):
elif self._revlist(not_rev(HEAD), revid):
return True
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')
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',
'subcmds']:
if not os.path.exists(os.path.join(my_dir, name)):

View File

@ -61,12 +61,34 @@ class Branches(Command):
%prog [<project>...]
Summarizes the currently available topic branches.
"""
def _Options(self, p):
p.add_option('-a', '--all',
dest='all', action='store_true',
help='show all branches, not just the majority')
Branch Display
--------------
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):
projects = self.GetProjects(args)
@ -84,18 +106,6 @@ Summarizes the currently available topic branches.
names = all.keys()
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:
print >>sys.stderr, ' (no branches)'
return
@ -126,7 +136,7 @@ Summarizes the currently available topic branches.
hdr('%c%c %-*s' % (current, published, width, name))
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
paths = []
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_PATH', project.relpath)
setenv('REPO_REMOTE', project.remote.name)
setenv('REPO_LREV', project\
.GetRemote(project.remote.name)\
.ToLocal(project.revision))
setenv('REPO_RREV', project.revision)
setenv('REPO_LREV', project.GetRevisionId())
setenv('REPO_RREV', project.revisionExpr)
if mirror:
setenv('GIT_DIR', project.gitdir)

View File

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

View File

@ -19,9 +19,8 @@ import sys
from color import Coloring
from command import InteractiveCommand, MirrorSafeCommand
from error import ManifestParseError
from remote import Remote
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):
common = True
@ -86,19 +85,6 @@ to update the working directory files.
dest='no_repo_verify', action='store_true',
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):
m = self.manifest.manifestProject
is_new = not m.Exists
@ -114,12 +100,12 @@ to update the working directory files.
m._InitGitDir()
if opt.manifest_branch:
m.revision = opt.manifest_branch
m.revisionExpr = opt.manifest_branch
else:
m.revision = 'refs/heads/master'
m.revisionExpr = 'refs/heads/master'
else:
if opt.manifest_branch:
m.revision = opt.manifest_branch
m.revisionExpr = opt.manifest_branch
else:
m.PreSync()
@ -162,20 +148,33 @@ to update the working directory files.
print >>sys.stderr, 'fatal: %s' % str(e)
sys.exit(1)
def _PromptKey(self, prompt, key, value):
def _Prompt(self, prompt, value):
mp = self.manifest.manifestProject
sys.stdout.write('%-10s [%s]: ' % (prompt, value))
a = sys.stdin.readline().strip()
if a != '' and a != value:
mp.config.SetString(key, a)
if a == '':
return value
return a
def _ConfigureUser(self):
mp = self.manifest.manifestProject
print ''
self._PromptKey('Your Name', 'user.name', mp.UserName)
self._PromptKey('Your Email', 'user.email', mp.UserEmail)
while True:
print ''
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):
for n in ['ui', 'diff', 'status']:
@ -215,7 +214,7 @@ to update the working directory files.
gc.SetString('color.ui', 'auto')
def Execute(self, opt, args):
self._CheckGitVersion()
git_require(MIN_GIT_VERSION, fail=True)
self._SyncManifest(opt)
self._LinkManifest(opt.manifest_name)

View File

@ -22,13 +22,18 @@ class Start(Command):
common = True
helpSummary = "Start a new branch for development"
helpUsage = """
%prog <newbranchname> [<project>...]
%prog <newbranchname> [--all | <project>...]
"""
helpDescription = """
'%prog' begins a new branch of development, starting from the
revision specified in the manifest.
"""
def _Options(self, p):
p.add_option('--all',
dest='all', action='store_true',
help='begin branch in all projects')
def Execute(self, opt, args):
if not args:
self.Usage()
@ -39,7 +44,14 @@ revision specified in the manifest.
sys.exit(1)
err = []
all = self.GetProjects(args[1:])
projects = []
if not opt.all:
projects = args[1:]
if len(projects) < 1:
print >>sys.stderr, "error: at least one project must be specified"
sys.exit(1)
all = self.GetProjects(projects)
pm = Progress('Starting %s' % nb, len(all))
for project in all:

View File

@ -16,12 +16,15 @@
from optparse import SUPPRESS_HELP
import os
import re
import shutil
import subprocess
import sys
import time
from git_command import GIT
from project import HEAD
from project import Project
from project import RemoteSpec
from command import Command, MirrorSafeCommand
from error import RepoChangedException, GitError
from project import R_HEADS
@ -108,7 +111,6 @@ later is required to fix a server side protocol bug.
pm = Progress('Fetching projects', len(projects))
for project in projects:
pm.update()
if project.Sync_NetworkHalf():
fetched.add(project.gitdir)
else:
@ -117,6 +119,61 @@ later is required to fix a server side protocol bug.
pm.end()
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):
if opt.network_only and opt.detach_head:
print >>sys.stderr, 'error: cannot combine -n and -d'
@ -134,6 +191,15 @@ later is required to fix a server side protocol bug.
if opt.repo_upgraded:
_PostRepoUpgrade(self.manifest)
if not opt.local_only:
mp.Sync_NetworkHalf()
if mp.HasChanges:
syncbuf = SyncBuffer(mp.config)
mp.Sync_LocalHalf(syncbuf)
if not syncbuf.Finish():
sys.exit(1)
self.manifest._Unload()
all = self.GetProjects(args, missing_ok=True)
if not opt.local_only:
@ -141,7 +207,6 @@ later is required to fix a server side protocol bug.
now = time.time()
if (24 * 60 * 60) <= (now - rp.LastFetch):
to_fetch.append(rp)
to_fetch.append(mp)
to_fetch.extend(all)
fetched = self._Fetch(to_fetch)
@ -150,12 +215,6 @@ later is required to fix a server side protocol bug.
# bail out now; the rest touches the working tree
return
if mp.HasChanges:
syncbuf = SyncBuffer(mp.config)
mp.Sync_LocalHalf(syncbuf)
if not syncbuf.Finish():
sys.exit(1)
self.manifest._Unload()
all = self.GetProjects(args, missing_ok=True)
missing = []
@ -164,6 +223,13 @@ later is required to fix a server side protocol bug.
missing.append(project)
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,
detach_head = opt.detach_head)
pm = Progress('Syncing work tree', len(all))
@ -176,7 +242,6 @@ later is required to fix a server side protocol bug.
if not syncbuf.Finish():
sys.exit(1)
def _PostRepoUpgrade(manifest):
for project in manifest.projects.values():
if project.Exists:
@ -207,17 +272,14 @@ def _VerifyTag(project):
warning: Cannot automatically authenticate repo."""
return True
remote = project.GetRemote(project.remote.name)
ref = remote.ToLocal(project.revision)
try:
cur = project.bare_git.describe(ref)
cur = project.bare_git.describe(project.GetRevisionId())
except GitError:
cur = None
if not 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):
rev = rev[len(R_HEADS):]

View File

@ -194,6 +194,18 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
_die("nothing uncommented for upload")
self._UploadAndReport(todo, people)
def _FindGerritChange(self, branch):
last_pub = branch.project.WasPublished(branch.name)
if last_pub is None:
return ""
refs = branch.GetPublishedRefs()
try:
# refs/changes/XYZ/N --> XYZ
return refs.get(last_pub).split('/')[-2]
except:
return ""
def _ReplaceBranch(self, project, people):
branch = project.CurrentBranch
if not branch:
@ -206,8 +218,14 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
script = []
script.append('# Replacing from branch %s' % branch.name)
for commit in branch.commits:
script.append('[ ] %s' % commit)
if len(branch.commits) == 1:
change = self._FindGerritChange(branch)
script.append('[%-6s] %s' % (change, branch.commits[0]))
else:
for commit in branch.commits:
script.append('[ ] %s' % commit)
script.append('')
script.append('# Insert change numbers in the brackets to add a new patch set.')
script.append('# To create a new change record, leave the brackets empty.')

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

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

52
tests/test_git_config.py Normal file
View File

@ -0,0 +1,52 @@
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')
def test_GetString_from_missing_file(self):
"""
Test missing config file
"""
config_fixture = fixture('not.present.gitconfig')
config = git_config.GitConfig(config_fixture)
val = config.GetString('empty')
self.assertEqual(val, None)
if __name__ == '__main__':
unittest.main()