Create the Next Generation of depot_tools. Eh.

Review URL: http://codereview.chromium.org/92087

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@14349 0039d316-1c4b-4281-b951-d872f2087c98
This commit is contained in:
maruel@google.com
2009-04-23 21:03:42 +00:00
commit fb2b8eb2e2
41 changed files with 11450 additions and 0 deletions

27
LICENSE Normal file
View File

@@ -0,0 +1,27 @@
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

39
README Normal file
View File

@@ -0,0 +1,39 @@
This package contains tools for working with Chromium development.
The "gclient" wrapper knows how to keep this repository updated to
the latest versions of these tools as found at:
http://src.chromium.org/svn/trunk/tools/depot_tools
This package contains:
chrome-update-create-task.bat
Creates a scheduled task to do an automatic local chromium build every day.
gcl
A tool for uploading and managing code reviews on the Chromium
project, using the Rietveld code review tool. More info at:
http://code.google.com/p/rietveld/
gclient
A script for managing a workspace with modular dependencies that
are each checked out independently from different repositories.
More info at:
http://code.google.com/p/gclient/
hammer
A wrapper script for building Chromium with the SCons software
construction tool. More info at:
http://www.scons.org/
revert
A small tool to quickly revert a change.
Note: svn and python will be installed automatically if not accessible (on
Windows only).
To update this distribution manually, run bootstrap\update.bat on Windows,
or bootstrap/update.sh on Linux or Mac.
To disable automatic updating, set the environment variable DEPOT_TOOLS_UPDATE=1

52
README.git-cl Normal file
View File

@@ -0,0 +1,52 @@
# git-cl -- a git-command for integrating reviews on Rietveld
# Copyright (C) 2008 Evan Martin <martine@danga.com>
== Background
Rietveld, also known as http://codereview.appspot.com, is a nice tool
for code reviews. You upload a patch (and some other data) and it lets
others comment on your patch.
For more on how this all works conceptually, please see README.codereview.
The remainder of this document is the nuts and bolts of using git-cl.
== Install
Copy (symlink) it into your path somewhere, along with Rietveld
upload.py.
== Setup
Run this from your git checkout and answer some questions:
$ git cl config
== How to use it
Make a new branch. Write some code. Commit it locally. Send it for
review:
$ git cl upload
By default, it diffs against whatever branch the current branch is
tracking (see "git checkout --track"). An optional last argument is
passed to "git diff", allowing reviews against other heads.
You'll be asked some questions, and the review issue number will be
associated with your current git branch, so subsequent calls to upload
will update that review rather than making a new one.
== git-svn integration
Review looks good? Commit the code:
$ git cl dcommit
This does a git-svn dcommit, with a twist: all changes in the diff
will be squashed into a single commit, and the description of the commit
is taken directly from the Rietveld description. This command also accepts
arguments to "git diff", much like upload.
Try "git cl dcommit --help" for more options.
== Extra commands
Print some status info:
$ git cl status
Edit the issue association on the current branch:
$ git cl issue 1234
Patch in a review:
$ git cl patch <url to full patch>
Try "git cl patch --help" for more options.
vim: tw=72 :

99
README.git-cl.codereview Normal file
View File

@@ -0,0 +1,99 @@
The git-cl README describes the git-cl command set. This document
describes how code review and git work together in general, intended
for people familiar with git but unfamiliar with the code review
process supported by Rietveld.
== Concepts and terms
A Rietveld review is for discussion of a single change or patch. You
upload a proposed change, the reviewer comments on your change, and
then you can upload a revised version of your change. Rietveld stores
the history of uploaded patches as well as the comments, and can
compute diffs in between these patches. The history of a patch is
very much like a small branch in git, but since Rietveld is
VCS-agnostic the concepts don't map perfectly. The identifier for a
single review+patches+comments in Rietveld is called an "issue".
Rietveld provides a basic uploader that understands git. This program
is used by git-cl, and is included in the git-cl repo as upload.py.
== Basic interaction with git
The fundamental problem you encounter when you try to mix git and code
review is that with git it's nice to commit code locally, while during
a code review you're often requested to change something about your
code. There are a few different ways you can handle this workflow
with git:
1) Rewriting a single commit. Say the origin commit is O, and you
commit your initial work in a commit A, making your history like
O--A. After review comments, you commit --amend, effectively
erasing A and making a new commit A', so history is now O--A'.
(Equivalently, you can use git reset --soft or git rebase -i.)
2) Writing follow-up commits. Initial work is again in A, and after
review comments, you write a new commit B so your history looks
like O--A--B. When you upload the revised patch, you upload the
diff of O..B, not A..B; you always upload the full diff of what
you're proposing to change.
The Rietveld patch uploader just takes arguments to "git diff", so
either of the above workflows work fine. If all you want to do is
upload a patch, you can use the upload.py provided by Rietveld with
arguments like this:
upload.py --server server.com <args to "git diff">
The first time you upload, it creates a new issue; for follow-ups on
the same issue, you need to provide the issue number:
upload.py --server server.com --issue 1234 <args to "git diff">
== git-cl to the rescue
git-cl simplifies the above in the following ways:
1) "git cl config" puts a persistent --server setting in your .git/config.
2) The first time you upload an issue, the issue number is associated with
the current *branch*. If you upload again, it will upload on the same
issue. (Note that this association is tied to a branch, not a commit,
which means you need a separate branch per review.)
3) If your branch is "tracking" (in the "git checkout --track" sense)
another one (like origin/master), calls to "git cl upload" will
diff against that branch by default. (You can still pass arguments
to "git diff" on the command line, if necessary.)
In the common case, this means that calling simply "git cl upload"
will always upload the correct diff to the correct place.
== Patch series
The above is all you need to know for working on a single patch.
Things get much more complicated when you have a series of commits
that you want to get reviewed. Say your history looks like
O--A--B--C. If you want to upload that as a single review, everything
works just as above.
But what if you upload each of A, B, and C as separate reviews?
What if you then need to change A?
1) One option is rewriting history: write a new commit A', then use
git rebase -i to insert that diff in as O--A--A'--B--C as well as
squash it. This is sometimes not possible if B and C have touched
some lines affected by A'.
2) Another option, and the one espoused by software like topgit, is for
you to have separate branches for A, B, and C, and after writing A'
you merge it into each of those branches. (topgit automates this
merging process.) This is also what is recommended by git-cl, which
likes having different branch identifiers to hang the issue number
off of. Your history ends up looking like:
O---A---B---C
\ \ \
A'--B'--C'
Which is ugly, but it accurately tracks the real history of your work, can
be thrown away at the end by committing A+A' as a single "squash" commit.
In practice, this comes up pretty rarely. Suggestions for better workflows
are welcome.

21
bootstrap/gclient.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
:: Copyright (c) 2009 The Chromium Authors. All rights reserved.
:: Use of this source code is governed by a BSD-style license that can be
:: found in the LICENSE file.
:: This batch file will try to sync the root directory and call back gclient.
setlocal
:: Shall skip automatic update?
IF "%DEPOT_TOOLS_UPDATE%" == "0" GOTO gclient
:: We can't sync if ..\.svn\. doesn't exist.
IF NOT EXIST "%~dp0..\.svn" GOTO gclient
:: Sync the .. directory to update the bootstrap at the same time.
call svn -q up "%~dp0.."
:gclient
:: Defer control.
python "%~dp0\..\gclient.py" %*

18
bootstrap/gclient.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
# Copyright (c) 2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This script will try to sync the root and bootstrap directories.
base_dir=$(dirname "$0")
# Skip if $DEPOT_TOOLS_UPDATE==0 or ../.svn/. doesn't exist.
if [ "X$DEPOT_TOOLS_UPDATE" != "X0" -a -e "$base_dir/../.svn" ]
then
# Update the root directory.
svn -q up "$base_dir/.."
fi
exec python "$base_dir/../gclient.py" "$@"

View File

@@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -0,0 +1,30 @@
7-Zip Command line version
~~~~~~~~~~~~~~~~~~~~~~~~~~
License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7-Zip Copyright (C) 1999-2009 Igor Pavlov.
7za.exe is distributed under the GNU LGPL license
Notes:
You can use 7-Zip on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip.
GNU LGPL information
--------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

View File

@@ -0,0 +1,42 @@
7-Zip Command line version 4.65
-------------------------------
7-Zip is a file archiver with high compression ratio.
7za.exe is a standalone command line version of 7-Zip.
7-Zip Copyright (C) 1999-2009 Igor Pavlov.
Features of 7za.exe:
- High compression ratio in new 7z format
- Supported formats:
- Packing / unpacking: 7z, ZIP, GZIP, BZIP2 and TAR
- Unpacking only: Z
- Highest compression ratio for ZIP and GZIP formats.
- Fast compression and decompression
- Strong AES-256 encryption in 7z and ZIP formats.
7za.exe is a free software distributed under the GNU LGPL.
Read license.txt for more information.
Source code of 7za.exe and 7-Zip can be found at
http://www.7-zip.org/
7za.exe can work in Windows 95/98/ME/NT/2000/XP/2003/Vista.
There is also port of 7za.exe for POSIX systems like Unix (Linux, Solaris, OpenBSD,
FreeBSD, Cygwin, AIX, ...), MacOS X and BeOS:
http://p7zip.sourceforge.net/
This distributive packet contains the following files:
7za.exe - 7-Zip standalone command line version.
readme.txt - This file.
copying.txt - GNU LGPL license.
license.txt - License information.
7-zip.chm - User's Manual in HTML Help format.
---
End of document

BIN
bootstrap/win/7za.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,16 @@
Including the following third parties:
7zip version 4.65
7za.exe is a free software distributed under the GNU LGPL.
Read license.txt for more information.
Source code of 7za.exe and 7-Zip can be found at
http://www.7-zip.org/
Note: packed with UPX to reduce code size.
wget 1.11.4 from http://www.gnu.org/software/wget/
Note: local compile without openssl support to reduce code size.
Note: packed with UPX to reduce code size.

1
bootstrap/win/python.bat Normal file
View File

@@ -0,0 +1 @@
@"%~dp0python\python.exe" %*

1
bootstrap/win/svn.bat Normal file
View File

@@ -0,0 +1 @@
@"%~dp0svn\svn.exe" %*

BIN
bootstrap/win/wget.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,65 @@
@echo off
:: Copyright (c) 2009 The Chromium Authors. All rights reserved.
:: Use of this source code is governed by a BSD-style license that can be
:: found in the LICENSE file.
:: This script will try to find if svn and python are accessible and it not,
:: it will try to download it and 'install' it in depot_tools.
SETLOCAL
set ROOT_URL=http://src.chromium.org/svn/trunk/tools
set ROOT_DIR=%~dp0..\..
:: If the batch file exists, skip the svn check.
if exist "%ROOT_DIR%\svn.bat" goto :PYTHON
call svn --version 2>nul 1>nul
if errorlevel 1 call :C_SVN
:PYTHON
:: If the batch file exists, skip the python check.
if exist "%ROOT_DIR%\python.bat" goto :EOF
call python --version 2>nul 1>nul
if errorlevel 1 call :C_PYTHON
:: We are done.
goto :EOF
:C_SVN
echo Installing subversion ...
:: svn is not accessible; check it out and create 'proxy' files.
call "%~dp0wget" -q %ROOT_URL%/third_party/svn.7z -O "%~dp0svn.7z"
if errorlevel 1 goto :SVN_FAIL
call "%~dp07za" x "%~dp0svn.7z" %ROOT_DIR%
if errorlevel 1 goto :SVN_FAIL
del "%~dp0svn.7z"
:: Create the batch file.
copy "%~dp0svn.bat" "%ROOT_DIR%"
goto :EOF
:SVN_FAIL
echo Failed to checkout svn automatically.
echo Please visit http://subversion.tigris.org to download the latest subversion client
echo before continuing.
echo You can also get the "prebacked" version used at %ROOT_URL%/third_party/
:: Still try python.
goto :C_PYTHON
:C_PYTHON
echo Installing python ...
call svn co %ROOT_URL%/third_party/python "%ROOT_DIR%\python"
if errorlevel 1 goto :PYTHON_FAIL
:: Create the batch file.
copy "%~dp0python.bat" "%ROOT_DIR%"
goto :EOF
:PYTHON_FAIL
echo Failed to checkout python automatically.
echo Please visit http://python.org to download the latest python 2.x client before
echo continuing.
echo You can also get the "prebacked" version used at %ROOT_URL%/third_party/
goto :EOF

View File

@@ -0,0 +1,53 @@
@echo off
:: Copyright (c) 2009 The Chromium Authors. All rights reserved.
:: Use of this source code is governed by a BSD-style license that can be
:: found in the LICENSE file.
setlocal
:: This script will create a scheduled task to run chrome-update every day
:: at the time you specify. This script expects to be live in
:: depot_tools\latest.
::
:: Usage: this-script <time to run task> <path to chrome trunk>
set Out=%USERPROFILE%\chrome-update-task.bat
set TaskTime=%1
set Trunk=%~f2
if not exist "%Trunk%" (
echo Usage: %~n0 ^<time^> ^<c:\path\to\chrome\trunk^>
echo ^<time^> is the time in HH:MM:SS format at which to run the task.
echo Example: %~n0 02:00:00 c:\src\chrome\trunk
exit 1
)
if not exist "%Out%" goto CreateScript
echo WARNING: %Out% already exists.
set Choice=
set /P Choice=Overwrite file [Y/N]?
if not "%Choice%"=="y" goto CreateTask
:CreateScript
echo.
echo Creating %Out%
echo>"%Out%" @echo off
echo>>"%Out%" call "%~dp0\bootstrap\update.bat"
echo>>"%Out%" "%~dp0\chrome-update.bat" "%Trunk%" ^> "%Trunk%\chrome-update-results.txt"
:CreateTask
echo.
echo ***********************************************************************
echo Creating a Scheduled Task to run chrome-update each day at %TaskTime%.
echo The batch file being run will live at %Out%.
echo.
echo WARNING: The password you enter will be displayed in cleartext.
echo If you're paranoid, you can enter blank here and then fix the password
echo by editing the scheduled task manually from the Control Panel.
echo ***********************************************************************
echo.
schtasks /create /tn chrome-update /tr "\"%Out%\"" /sc daily /st %TaskTime%

10
chrome-update.bat Normal file
View File

@@ -0,0 +1,10 @@
@echo off
:: This batch file assumes that the correct version of python can be found in
:: the current directory, and that you have Visual Studio 8 installed in the
:: default location.
setlocal
call vcvars32.bat
python "%~dp0chrome-update.py" %*

170
chrome-update.py Executable file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/python
# Copyright (c) 2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Author: mpcomplete
#
# This script updates and does a clean build of chrome for you.
# Usage: python chrome-update.py C:\path\to\chrome\trunk
#
# It assumes the following:
# - You have gclient.bat and devenv.com in your path (use the wrapper batch
# file to ensure this).
import sys
import os
import subprocess
import httplib
import re
import shutil
import optparse
def Message(str):
"""Prints a status message."""
print "[chrome-update]", str
def FixupPath(path):
"""Returns the OS-ified version of a windows path."""
return os.path.sep.join(path.split("\\"))
def GetRevision():
"""Returns the revision number of the last build that was archived, or
None on failure."""
HOST = "build.chromium.org"
PATH = "/buildbot/continuous/LATEST/REVISION"
EXPR = r"(\d+)"
connection = httplib.HTTPConnection(HOST)
connection.request("GET", PATH)
response = connection.getresponse()
text = response.read()
match = re.search(EXPR, text)
if match:
return int(match.group(1))
return None
def SetRevisionForUpdate(chrome_root):
"""Prepares environment so gclient syncs to a good revision, if possible."""
# Find a buildable revision.
rev = GetRevision()
if rev == None:
Message("WARNING: Failed to find a buildable revision. Syncing to trunk.")
return "trunk"
# Read the .gclient file.
gclient_file = chrome_root + FixupPath("\\.gclient")
if not os.path.exists(gclient_file):
Message("WARNING: Failed to find .gclient file. Syncing to trunk.")
return "trunk"
scope = {}
execfile(gclient_file, scope)
solutions = scope["solutions"]
# Edit the url of the chrome 'src' solution, unless the user wants a
# specific revision.
for solution in solutions:
if solution["name"] == "src":
splitter = solution["url"].split("@")
if len(splitter) == 1:
solution["url"] = splitter[0] + "@" + str(rev)
else:
rev = int(splitter[1])
break
# Write out the new .gclient file.
gclient_override = gclient_file + "-update-chrome"
f = open(gclient_override, "w")
f.write("solutions = " + str(solutions))
f.close()
# Set the env var that the gclient tool looks for.
os.environ["GCLIENT_FILE"] = gclient_override
return rev
def DoUpdate(chrome_root):
"""gclient sync to the latest build."""
# gclient sync
rev = SetRevisionForUpdate(chrome_root)
cmd = ["gclient.bat", "sync"]
Message("Updating to %s: %s" % (rev, cmd))
sys.stdout.flush()
return subprocess.call(cmd, cwd=chrome_root)
def DoClean(chrome_root, type):
"""Clean our build dir."""
# rm -rf src/chrome/Debug
rv = [0]
def onError(func, path, excinfo):
Message("Couldn't remove '%s': %s" % (path, excinfo))
rv[0] = [1]
build_path = chrome_root + FixupPath("\\src\\chrome\\" + type)
Message("Cleaning: %s" % build_path)
shutil.rmtree(build_path, False, onError)
return rv[0]
def DoBuild(chrome_root, chrome_sln, clean, type):
"""devenv /build what we just checked out."""
if clean:
rv = DoClean(chrome_root, type)
if rv != 0:
Message("WARNING: Clean failed. Doing a build without clean.")
# devenv chrome.sln /build Debug
cmd = ["devenv.com", chrome_sln, "/build", type]
Message("Building: %s" % cmd)
sys.stdout.flush()
return subprocess.call(cmd, cwd=chrome_root)
def Main():
parser = optparse.OptionParser()
parser.add_option("", "--clean", action="store_true", default=False,
help="wipe Debug output directory before building")
parser.add_option("", "--solution", default="src\\chrome\\chrome.sln",
help="path to the .sln file to build (absolute, or "
"relative to chrome trunk")
parser.add_option("", "--release", action="store_true", default=False,
help="build the release configuration in addition of the "
"debug configuration.")
parser.add_option("", "--nosync", action="store_true", default=False,
help="doesn't sync before building")
parser.add_option("", "--print-latest", action="store_true", default=False,
help="print the latest buildable revision and exit")
options, args = parser.parse_args()
if options.print_latest:
print GetRevision() or "HEAD"
sys.exit(0)
if not args:
Message("Usage: %s <path\\to\\chrome\\root> [options]" % sys.argv[0])
sys.exit(1)
chrome_root = args[0]
if not options.nosync:
rv = DoUpdate(chrome_root)
if rv != 0:
Message("Update Failed. Bailing.")
sys.exit(rv)
chrome_sln = FixupPath(options.solution)
rv = DoBuild(chrome_root, chrome_sln, options.clean, "Debug")
if rv != 0:
Message("Debug build failed. Sad face :(")
if options.release:
rv = DoBuild(chrome_root, chrome_sln, options.clean, "Release")
if rv != 0:
Message("Release build failed. Sad face :(")
if rv != 0:
sys.exit(rv)
Message("Success!")
if __name__ == "__main__":
Main()

2725
cpplint.py vendored Executable file

File diff suppressed because it is too large Load Diff

5
gcl Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
base_dir=$(dirname "$0")
exec python "$base_dir/gcl.py" "$@"

1
gcl.bat Normal file
View File

@@ -0,0 +1 @@
@call python "%~dp0gcl.py" %*

1122
gcl.py Executable file

File diff suppressed because it is too large Load Diff

21
gclient Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# Copyright (c) 2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This script will try to sync the bootstrap directories and then defer control.
base_dir=$(dirname "$0")
if [ "X$DEPOT_TOOLS_UPDATE" != "X0" -a -e "$base_dir/.svn" ]
then
# Update the bootstrap directory to stay up-to-date with the latest
# depot_tools.
svn -q up "$base_dir/bootstrap"
# Then defer the control to the bootstrapper.
"$base_dir/bootstrap/gclient.sh"
else
exec "$base_dir/gclient.py" "$@"
fi

36
gclient.bat Normal file
View File

@@ -0,0 +1,36 @@
@echo off
:: Copyright (c) 2009 The Chromium Authors. All rights reserved.
:: Use of this source code is governed by a BSD-style license that can be
:: found in the LICENSE file.
:: This file is a stub to sync .\bootstrap first and defer control to
:: .\bootstrap\gclient.bat, which will sync back '.'. This is unless auto
:: update is disabled, were gclient.py is directly called.
:: Shall skip automatic update?
IF "%DEPOT_TOOLS_UPDATE%" == "0" GOTO :SKIP_UPDATE
:: We can't sync if .\.svn\. doesn't exist.
IF NOT EXIST "%~dp0.svn" GOTO :SKIP_UPDATE
:: Will download svn and python if not already installed on the system.
call "%~dp0bootstrap\win\win_tools.bat"
if errorlevel 1 goto :EOF
:: Sync the bootstrap directory *only after*.
call svn up -q "%~dp0bootstrap"
:: still continue even in case of error.
goto :UPDATE
:SKIP_UPDATE
:: Don't bother to try to update any thing.
python "%~dp0\gclient.py" %*
goto :EOF
:UPDATE
:: Transfer control to ease the update process. The following lines won't be
:: executed so don't add any! Specifically, don't use 'call' in the following
:: line.
"%~dp0bootstrap\gclient.bat" %*
goto :EOF

1649
gclient.py Executable file

File diff suppressed because it is too large Load Diff

682
git-cl.py Executable file
View File

@@ -0,0 +1,682 @@
#!/usr/bin/python
# git-cl -- a git-command for integrating reviews on Rietveld
# Copyright (C) 2008 Evan Martin <martine@danga.com>
import getpass
import optparse
import os
import re
import readline
import subprocess
import sys
import tempfile
import textwrap
import upload
import urllib2
DEFAULT_SERVER = 'codereview.appspot.com'
def DieWithError(message):
print >>sys.stderr, message
sys.exit(1)
def RunGit(args, error_ok=False, error_message=None, exit_code=False):
cmd = ['git'] + args
# Useful for debugging:
# print >>sys.stderr, ' '.join(cmd)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
output = proc.communicate()[0]
if exit_code:
return proc.returncode
if not error_ok and proc.returncode != 0:
DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) +
(error_message or output))
return output
class Settings:
def __init__(self):
self.server = None
self.cc = None
self.is_git_svn = None
self.svn_branch = None
self.tree_status_url = None
self.viewvc_url = None
def GetServer(self, error_ok=False):
if not self.server:
if not error_ok:
error_message = ('You must configure your review setup by running '
'"git cl config".')
self.server = self._GetConfig('rietveld.server',
error_message=error_message)
else:
self.server = self._GetConfig('rietveld.server', error_ok=True)
return self.server
def GetCCList(self):
if self.cc is None:
self.cc = self._GetConfig('rietveld.cc', error_ok=True)
return self.cc
def GetIsGitSvn(self):
"""Return true if this repo looks like it's using git-svn."""
if self.is_git_svn is None:
# If you have any "svn-remote.*" config keys, we think you're using svn.
self.is_git_svn = RunGit(['config', '--get-regexp', r'^svn-remote\.'],
exit_code=True) == 0
return self.is_git_svn
def GetSVNBranch(self):
if self.svn_branch is None:
if not self.GetIsGitSvn():
raise "Repo doesn't appear to be a git-svn repo."
# Try to figure out which remote branch we're based on.
# Strategy:
# 1) find all git-svn branches and note their svn URLs.
# 2) iterate through our branch history and match up the URLs.
# regexp matching the git-svn line that contains the URL.
git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
# Get the refname and svn url for all refs/remotes/*.
remotes = RunGit(['for-each-ref', '--format=%(refname)',
'refs/remotes']).splitlines()
svn_refs = {}
for ref in remotes:
# git-svn remote refs are generally directly in the refs/remotes/dir,
# not a subdirectory (like refs/remotes/origin/master).
if '/' in ref[len('refs/remotes/'):]:
continue
match = git_svn_re.search(RunGit(['cat-file', '-p', ref]))
if match:
svn_refs[match.group(1)] = ref
if len(svn_refs) == 1:
# Only one svn branch exists -- seems like a good candidate.
self.svn_branch = svn_refs.values()[0]
elif len(svn_refs) > 1:
# We have more than one remote branch available. We don't
# want to go through all of history, so read a line from the
# pipe at a time.
# The -100 is an arbitrary limit so we don't search forever.
cmd = ['git', 'log', '-100']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in proc.stdout:
match = git_svn_re.match(line)
if match:
url = match.group(1)
if url in svn_refs:
self.svn_branch = svn_refs[url]
proc.stdout.close() # Cut pipe.
break
if not self.svn_branch:
raise "Can't guess svn branch -- try specifying it on the command line"
return self.svn_branch
def GetTreeStatusUrl(self, error_ok=False):
if not self.tree_status_url:
error_message = ('You must configure your tree status URL by running '
'"git cl config".')
self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
error_ok=error_ok,
error_message=error_message)
return self.tree_status_url
def GetViewVCUrl(self):
if not self.viewvc_url:
self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
return self.viewvc_url
def _GetConfig(self, param, **kwargs):
return RunGit(['config', param], **kwargs).strip()
settings = Settings()
did_migrate_check = False
def CheckForMigration():
"""Migrate from the old issue format, if found.
We used to store the branch<->issue mapping in a file in .git, but it's
better to store it in the .git/config, since deleting a branch deletes that
branch's entry there.
"""
# Don't run more than once.
global did_migrate_check
if did_migrate_check:
return
gitdir = RunGit(['rev-parse', '--git-dir']).strip()
storepath = os.path.join(gitdir, 'cl-mapping')
if os.path.exists(storepath):
print "old-style git-cl mapping file (%s) found; migrating." % storepath
store = open(storepath, 'r')
for line in store:
branch, issue = line.strip().split()
RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
issue])
store.close()
os.remove(storepath)
did_migrate_check = True
def IssueURL(issue):
"""Get the URL for a particular issue."""
return 'http://%s/%s' % (settings.GetServer(), issue)
def ShortBranchName(branch):
"""Convert a name like 'refs/heads/foo' to just 'foo'."""
return branch.replace('refs/heads/', '')
class Changelist:
def __init__(self, branchref=None):
# Poke settings so we get the "configure your server" message if necessary.
settings.GetServer()
self.branchref = branchref
if self.branchref:
self.branch = ShortBranchName(self.branchref)
else:
self.branch = None
self.upstream_branch = None
self.has_issue = False
self.issue = None
self.has_description = False
self.description = None
def GetBranch(self):
"""Returns the short branch name, e.g. 'master'."""
if not self.branch:
self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
self.branch = ShortBranchName(self.branchref)
return self.branch
def GetBranchRef(self):
"""Returns the full branch name, e.g. 'refs/heads/master'."""
self.GetBranch() # Poke the lazy loader.
return self.branchref
def GetUpstreamBranch(self):
if self.upstream_branch is None:
branch = self.GetBranch()
upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
error_ok=True).strip()
if upstream_branch:
remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
# We have remote=origin and branch=refs/heads/foobar; convert to
# refs/remotes/origin/foobar.
self.upstream_branch = upstream_branch.replace('heads',
'remotes/' + remote)
if not self.upstream_branch:
# Fall back on trying a git-svn upstream branch.
if settings.GetIsGitSvn():
self.upstream_branch = settings.GetSVNBranch()
if not self.upstream_branch:
DieWithError("""Unable to determine default branch to diff against.
Either pass complete "git diff"-style arguments, like
git cl upload origin/master
or verify this branch is set up to track another (via the --track argument to
"git checkout -b ...").""")
return self.upstream_branch
def GetIssue(self):
if not self.has_issue:
CheckForMigration()
issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
if issue:
self.issue = issue
else:
self.issue = None
self.has_issue = True
return self.issue
def GetIssueURL(self):
return IssueURL(self.GetIssue())
def GetDescription(self, pretty=False):
if not self.has_description:
if self.GetIssue():
url = self.GetIssueURL() + '/description'
self.description = urllib2.urlopen(url).read().strip()
self.has_description = True
if pretty:
wrapper = textwrap.TextWrapper()
wrapper.initial_indent = wrapper.subsequent_indent = ' '
return wrapper.fill(self.description)
return self.description
def GetPatchset(self):
if not self.has_patchset:
patchset = RunGit(['config', self._PatchsetSetting()],
error_ok=True).strip()
if patchset:
self.patchset = patchset
else:
self.patchset = None
self.has_patchset = True
return self.patchset
def SetPatchset(self, patchset):
"""Set this branch's patchset. If patchset=0, clears the patchset."""
if patchset:
RunGit(['config', self._PatchsetSetting(), str(patchset)])
else:
RunGit(['config', '--unset', self._PatchsetSetting()])
self.has_patchset = False
def SetIssue(self, issue):
"""Set this branch's issue. If issue=0, clears the issue."""
if issue:
RunGit(['config', self._IssueSetting(), str(issue)])
else:
RunGit(['config', '--unset', self._IssueSetting()])
self.SetPatchset(0)
self.has_issue = False
def CloseIssue(self):
def GetUserCredentials():
email = raw_input('Email: ').strip()
password = getpass.getpass('Password for %s: ' % email)
return email, password
rpc_server = upload.HttpRpcServer(settings.GetServer(),
GetUserCredentials,
host_override=settings.GetServer(),
save_cookies=True)
# You cannot close an issue with a GET.
# We pass an empty string for the data so it is a POST rather than a GET.
data = [("description", self.description),]
ctype, body = upload.EncodeMultipartFormData(data, [])
rpc_server.Send('/' + self.GetIssue() + '/close', body, ctype)
def _IssueSetting(self):
"""Return the git setting that stores this change's issue."""
return 'branch.%s.rietveldissue' % self.GetBranch()
def _PatchsetSetting(self):
"""Return the git setting that stores this change's most recent patchset."""
return 'branch.%s.rietveldpatchset' % self.GetBranch()
def CmdConfig(args):
server = settings.GetServer(error_ok=True)
prompt = 'Rietveld server (host[:port])'
prompt += ' [%s]' % (server or DEFAULT_SERVER)
newserver = raw_input(prompt + ': ')
if not server and not newserver:
newserver = DEFAULT_SERVER
if newserver and newserver != server:
RunGit(['config', 'rietveld.server', newserver])
def SetProperty(initial, caption, name):
prompt = caption
if initial:
prompt += ' ("x" to clear) [%s]' % initial
new_val = raw_input(prompt + ': ')
if new_val == 'x':
RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
elif new_val and new_val != initial:
RunGit(['config', 'rietveld.' + name, new_val])
SetProperty(settings.GetCCList(), 'CC list', 'cc')
SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
'tree-status-url')
SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
# TODO: configure a default branch to diff against, rather than this
# svn-based hackery.
def CmdStatus(args):
branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
if branches:
print 'Branches associated with reviews:'
for branch in sorted(branches.splitlines()):
cl = Changelist(branchref=branch)
print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
cl = Changelist()
print
print 'Current branch:',
if not cl.GetIssue():
print 'no issue assigned.'
return 0
print cl.GetBranch()
print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
print 'Issue description:'
print cl.GetDescription(pretty=True)
def CmdIssue(args):
cl = Changelist()
if len(args) > 0:
cl.SetIssue(int(args[0]))
print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
def UserEditedLog(starting_text):
"""Given some starting text, let the user edit it and return the result."""
editor = os.getenv('EDITOR', 'vi')
file = tempfile.NamedTemporaryFile()
filename = file.name
file.write(starting_text)
file.flush()
ret = subprocess.call(editor + ' ' + filename, shell=True)
if ret != 0:
return
text = open(filename).read()
file.close()
stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
return stripcomment_re.sub('', text).strip()
def CmdUpload(args):
parser = optparse.OptionParser(
usage='git cl upload [options] [args to "git diff"]')
parser.add_option('-m', dest='message', help='message for patch')
parser.add_option('-r', '--reviewers',
help='reviewer email addresses')
parser.add_option('--send-mail', action='store_true',
help='send email to reviewer immediately')
(options, args) = parser.parse_args(args)
cl = Changelist()
if not args:
# Default to diffing against the "upstream" branch.
args = [cl.GetUpstreamBranch()]
# --no-ext-diff is broken in some versions of Git, so try to work around
# this by overriding the environment (but there is still a problem if the
# git config key "diff.external" is used).
env = os.environ.copy()
if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', ] + args, env=env)
upload_args = ['--assume_yes'] # Don't ask about untracked files.
upload_args.extend(['--server', settings.GetServer()])
if options.reviewers:
upload_args.extend(['--reviewers', options.reviewers])
upload_args.extend(['--cc', settings.GetCCList()])
if options.message:
upload_args.extend(['--message', options.message])
if options.send_mail:
if not options.reviewers:
DieWithError("Must specify reviewers to send email.")
upload_args.append('--send_mail')
if cl.GetIssue():
upload_args.extend(['--issue', cl.GetIssue()])
print ("This branch is associated with issue %s. "
"Adding patch to that issue." % cl.GetIssue())
else:
# Construct a description for this change from the log.
# We need to convert diff options to log options.
log_args = []
if len(args) == 1 and not args[0].endswith('.'):
log_args = [args[0] + '..']
elif len(args) == 2:
log_args = [args[0] + '..' + args[1]]
else:
log_args = args[:] # Hope for the best!
desc = RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
initial_text = """# Enter a description of the change.
# This will displayed on the codereview site.
# The first line will also be used as the subject of the review."""
desc = UserEditedLog(initial_text + '\n' + desc)
if not desc:
print "Description empty; aborting."
return 1
subject = desc.splitlines()[0]
upload_args.extend(['--message', subject])
upload_args.extend(['--description', desc])
issue, patchset = upload.RealMain(['upload'] + upload_args + args)
if not cl.GetIssue():
cl.SetIssue(issue)
cl.SetPatchset(patchset)
def CmdDCommit(args):
parser = optparse.OptionParser(
usage='git cl dcommit [options] [git-svn branch to apply against]')
parser.add_option('-f', action='store_true', dest='force',
help="force yes to questions (don't prompt)")
parser.add_option('-c', dest='contributor',
help="external contributor for patch (appended to " +
"description)")
(options, args) = parser.parse_args(args)
cl = Changelist()
if not args:
# Default to merging against our best guess of the upstream branch.
args = [cl.GetUpstreamBranch()]
base_branch = args[0]
# It is important to have these checks at the top. Not only for user
# convenience, but also because the cl object then caches the correct values
# of these fields even as we're juggling branches for setting up the commit.
if not cl.GetIssue():
print 'Current issue unknown -- has this branch been uploaded?'
return 1
if not cl.GetDescription():
print 'No description set.'
print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
return 1
if RunGit(['diff-index', 'HEAD']):
print 'Cannot dcommit with a dirty tree. You must commit locally first.'
return 1
# This rev-list syntax means "show all commits not in my branch that
# are in base_branch".
upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
base_branch]).splitlines()
if upstream_commits:
print ('Base branch "%s" has %d commits '
'not in this branch.' % (base_branch, len(upstream_commits)))
print 'Run "git merge %s" before attempting to dcommit.' % base_branch
return 1
if not options.force:
# Check the tree status if the tree status URL is set.
status = GetTreeStatus()
if 'closed' == status:
print ('The tree is closed. Please wait for it to reopen. Use '
'"git cl dcommit -f" to commit on a closed tree.')
return 1
elif 'unknown' == status:
print ('Unable to determine tree status. Please verify manually and '
'use "git cl dcommit -f" to commit on a closed tree.')
description = cl.GetDescription()
description += "\n\nReview URL: %s" % cl.GetIssueURL()
if options.contributor:
description += "\nPatch from %s." % options.contributor
print 'Description:', repr(description)
branches = [base_branch, cl.GetBranchRef()]
if not options.force:
subprocess.call(['git', 'diff', '--stat'] + branches)
raw_input("About to commit; enter to confirm.")
# We want to squash all this branch's commits into one commit with the
# proper description.
# We do this by doing a "merge --squash" into a new commit branch, then
# dcommitting that.
MERGE_BRANCH = 'git-cl-commit'
# Delete the merge branch if it already exists.
if RunGit(['show-ref', '--quiet', '--verify', 'refs/heads/' + MERGE_BRANCH],
exit_code=True) == 0:
RunGit(['branch', '-D', MERGE_BRANCH])
# Stuff our change into the merge branch.
RunGit(['checkout', '-q', '-b', MERGE_BRANCH, base_branch])
RunGit(['merge', '--squash', cl.GetBranchRef()])
RunGit(['commit', '-m', description])
# dcommit the merge branch.
output = RunGit(['svn', 'dcommit'])
# And then swap back to the original branch and clean up.
RunGit(['checkout', '-q', cl.GetBranch()])
RunGit(['branch', '-D', MERGE_BRANCH])
if output.find("Committed r") != -1:
print "Closing issue (you may be prompted for your codereview password)..."
if cl.has_issue:
viewvc_url = settings.GetViewVCUrl()
if viewvc_url:
revision = re.compile(".*?\nCommitted r(\d+)",
re.DOTALL).match(output).group(1)
cl.description = (cl.description +
"\n\nCommitted: " + viewvc_url + revision)
cl.CloseIssue()
cl.SetIssue(0)
def CmdPatch(args):
parser = optparse.OptionParser(usage=('git cl patch [options] '
'<patch url or issue id>'))
parser.add_option('-b', dest='newbranch',
help='create a new branch off trunk for the patch')
parser.add_option('-f', action='store_true', dest='force',
help='with -b, clobber any existing branch')
parser.add_option('--reject', action='store_true', dest='reject',
help='allow failed patches and spew .rej files')
parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
help="don't commit after patch applies")
(options, args) = parser.parse_args(args)
if len(args) != 1:
return parser.print_help()
input = args[0]
if re.match(r'\d+', input):
# Input is an issue id. Figure out the URL.
issue = input
fetch = "curl --silent http://%s/%s" % (settings.GetServer(), issue)
grep = "grep -E -o '/download/issue[0-9]+_[0-9]+.diff'"
pipe = subprocess.Popen("%s | %s" % (fetch, grep), shell=True,
stdout=subprocess.PIPE)
path = pipe.stdout.read().strip()
url = 'http://%s%s' % (settings.GetServer(), path)
else:
# Assume it's a URL to the patch.
match = re.match(r'http://.*?/issue(\d+)_\d+.diff', input)
if match:
issue = match.group(1)
url = input
else:
print "Must pass an issue ID or full URL for 'Download raw patch set'"
return 1
if options.newbranch:
if options.force:
RunGit(['branch', '-D', options.newbranch], error_ok=True)
RunGit(['checkout', '-b', options.newbranch])
# Switch up to the top-level directory, if necessary, in preparation for
# applying the patch.
top = RunGit(['rev-parse', '--show-cdup']).strip()
if top:
os.chdir(top)
# Construct a pipeline to feed the patch into "git apply".
# We use "git apply" to apply the patch instead of "patch" so that we can
# pick up file adds.
# 1) Fetch the patch.
fetch = "curl --silent %s" % url
# 2) Munge the patch.
# Git patches have a/ at the beginning of source paths. We strip that out
# with a sed script rather than the -p flag to patch so we can feed either
# Git or svn-style patches into the same apply command.
gitsed = "sed -e 's|^--- a/|--- |; s|^+++ b/|+++ |'"
# 3) Apply the patch.
# The --index flag means: also insert into the index (so we catch adds).
apply = "git apply --index -p0"
if options.reject:
apply += " --reject"
subprocess.check_call(' | '.join([fetch, gitsed, apply]), shell=True)
# If we had an issue, commit the current state and register the issue.
if not options.nocommit:
RunGit(['commit', '-m', 'patch from issue %s' % issue])
cl = Changelist()
cl.SetIssue(issue)
print "Committed patch."
else:
print "Patch applied to index."
def CmdRebase(args):
# Provide a wrapper for git svn rebase to help avoid accidental
# git svn dcommit.
RunGit(['svn', 'rebase'])
def GetTreeStatus():
"""Fetches the tree status and returns either 'open', 'closed',
'unknown' or 'unset'."""
url = settings.GetTreeStatusUrl(error_ok=True)
if url:
status = urllib2.urlopen(url).read().lower()
if status.find('closed') != -1:
return 'closed'
elif status.find('open') != -1:
return 'open'
return 'unknown'
return 'unset'
def CmdTreeStatus(args):
status = GetTreeStatus()
if 'unset' == status:
print 'You must configure your tree status URL by running "git cl config".'
else:
print "The tree is %s" % status
def CmdUpstream(args):
cl = Changelist()
print cl.GetUpstreamBranch()
COMMANDS = [
('config', 'edit configuration for this tree', CmdConfig),
('status', 'show status of changelists', CmdStatus),
('issue', 'show/set current branch\'s issue number', CmdIssue),
('upload', 'upload the current changelist to codereview', CmdUpload),
('dcommit', 'commit the current changelist via git-svn', CmdDCommit),
('patch', 'patch in a code review', CmdPatch),
('rebase', 'rebase current branch on top of svn repo', CmdRebase),
('tree', 'show the status of the tree', CmdTreeStatus),
('upstream', 'print the name of the upstream branch, if any', CmdUpstream),
]
def Usage(name):
print 'usage: %s <command>' % name
print 'commands are:'
for name, desc, _ in COMMANDS:
print ' %-10s %s' % (name, desc)
sys.exit(1)
def main(argv):
if len(argv) < 2:
Usage(argv[0])
command = argv[1]
for name, _, func in COMMANDS:
if name == command:
return func(argv[2:])
print 'unknown command: %s' % command
Usage(argv[0])
if __name__ == '__main__':
sys.exit(main(sys.argv))

184
git-try.py Executable file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/python
# Repo lives in ~evanm/projects/git-try -- feel free to send patches.
import getpass
import optparse
import os
import subprocess
import tempfile
import traceback
import urllib
import sys
def Backquote(cmd):
"""Like running `cmd` in a shell script."""
return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0].strip()
def GetTryServerConfig():
"""Returns the dictionary of try server options or None if they
cannot be found."""
script_path = 'tools/tryserver/tryserver.py'
root_dir = Backquote(['git', 'rev-parse', '--show-cdup'])
try:
script_file = open(os.path.join(root_dir, script_path))
except IOError:
return None
locals = {}
try:
exec(script_file, locals)
except Exception, e:
return None
return locals
def GetBranchName():
"""Return name of current git branch."""
branch = Backquote(['git', 'symbolic-ref', 'HEAD'])
if not branch.startswith('refs/heads/'):
raise "Couldn't figure out branch name"
branch = branch[len('refs/heads/'):]
return branch
def GetPatchName():
"""Construct a name for this patch."""
# TODO: perhaps include the hash of the current commit, to distinguish
# patches?
return GetBranchName()
def GetRevision():
"""Get the latest Subversion revision number."""
for line in Backquote(['git', 'svn', 'info']).split('\n'):
if line.startswith('Revision:'):
return line[len('Revision:'):].strip()
raise "Couldn't figure out latest revision"
def GetRietveldIssueNumber():
return Backquote(['git', 'config',
'branch.%s.rietveldissue' % GetBranchName()])
def GetRietveldPatchsetNumber():
return Backquote(['git', 'config',
'branch.%s.rietveldpatchset' % GetBranchName()])
def GetMungedDiff(branch):
"""Get the diff we'll send to the try server. We munge paths to match svn."""
# Make the following changes:
# - Prepend "src/" to paths as svn is expecting
# - In the case of added files, replace /dev/null with the path to the file
# being added.
output = []
if not branch:
# Try to guess the upstream branch.
branch = Backquote(['git', 'cl', 'upstream'])
diff = subprocess.Popen(['git', 'diff-tree', '-p', '--no-prefix',
branch, 'HEAD'],
stdout=subprocess.PIPE).stdout.readlines()
for i in range(len(diff)):
line = diff[i]
if line.startswith('--- /dev/null'):
line = '--- %s' % diff[i+1][4:]
elif line.startswith('--- ') or line.startswith('+++ '):
line = line[0:4] + 'src/' + line[4:]
output.append(line)
return ''.join(output)
def GetEmail():
# TODO: check for errors here?
return Backquote(['git', 'config', 'user.email'])
def TryChange(args):
"""Put a patch on the try server using SVN."""
# TODO: figure out a better way to load trychange
script_path = '../depot_tools/release'
root_dir = Backquote(['git', 'rev-parse', '--show-cdup'])
sys.path.append(os.path.join(root_dir, script_path))
import trychange
trychange.checkout_root = os.path.abspath(root_dir)
trychange.TryChange(args, None, False)
def WriteTryDiffHTTP(config, patch_name, diff, options):
"""Put a patch on the try server."""
params = {
'user': getpass.getuser(),
'name': patch_name,
'patch': diff
}
if options.bot:
params['bot'] = options.bot
if options.clobber:
params['clobber'] = 'true'
url = 'http://%s:%s/send_try_patch' % (config['try_server_http_host'],
config['try_server_http_port'])
connection = urllib.urlopen(url, urllib.urlencode(params))
response = connection.read()
if (response != 'OK'):
print "Error posting to", url
print response
assert False
if __name__ == '__main__':
parser = optparse.OptionParser(
usage='git try [branch]',
description='Upload the current diff of branch...HEAD to the try server.')
parser.add_option("-b", "--bot",
help="Force the use of a specific build slave (eg mac, "
"win, or linux)")
parser.add_option("-c", "--clobber", action="store_true",
help="Make the try run use be a clobber build")
(options, args) = parser.parse_args(sys.argv)
branch = None
if len(args) > 1:
branch = args[1]
patch_name = GetPatchName()
diff = GetMungedDiff(branch)
# Send directly to try server if we can parse the config, otherwise
# upload via SVN.
config = GetTryServerConfig()
if config is not None:
print "Sending %s using HTTP..." % patch_name
WriteTryDiffHTTP(config=config, patch_name=patch_name, diff=diff,
options=options)
else:
print "Sending %s using SVN..." % patch_name
# Write the diff out to a temporary file
diff_file = tempfile.NamedTemporaryFile()
diff_file.write(diff)
diff_file.flush()
email = GetEmail()
user = email.partition('@')[0]
args = [
'--use_svn',
'--svn_repo', 'svn://svn.chromium.org/chrome-try/try',
'-u', user,
'-e', email,
'-n', patch_name,
'-r', GetRevision(),
'--diff', diff_file.name,
]
if GetRietveldPatchsetNumber():
args.extend([
'--issue', GetRietveldIssueNumber(),
'--patchset', GetRietveldPatchsetNumber(),
])
TryChange(args=args)

9
hammer Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
# The first expression catches when we're actually in the /src directory.
# The second expressions strips everything after the last /src occurrence.
SRC_DIR=`pwd | sed -e '\;/src$;q' -e 's;\(.*/src\)/.*;\1;'`
SCONS="${SRC_DIR}/third_party/scons/scons.py"
SITE_SCONS="${SRC_DIR}/site_scons"
exec python "${SCONS}" "--site-dir=${SITE_SCONS}" "$@"

23
hammer.bat Normal file
View File

@@ -0,0 +1,23 @@
@echo off
@rem We're in a submodule directory, look relative to the parent.
call python "%cd%\..\third_party\scons\scons.py" "--site-dir=..\site_scons" %*
goto omega
:srcdir
call python "%cd%\third_party\scons\scons.py" --site-dir=site_scons %*
goto omega
@rem Per the following page:
@rem http://code-bear.com/bearlog/2007/06/01/getting-the-exit-code-from-a-batch-file-that-is-run-from-a-python-program/
@rem Just calling "exit /b" passes back an exit code, but in a way
@rem that does NOT get picked up correctly when executing the .bat
@rem file from the Python subprocess module. Using "call" as the
@rem last command in the .bat file makes it work as expected.
:returncode
exit /b %ERRORLEVEL%
:omega
call :returncode %ERRORLEVEL%

715
presubmit.py Executable file
View File

@@ -0,0 +1,715 @@
#!/usr/bin/python
# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Enables directory-specific presubmit checks to run at upload and/or commit.
"""
__version__ = '1.0'
# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
# caching (between all different invocations of presubmit scripts for a given
# change). We should add it as our presubmit scripts start feeling slow.
import cPickle # Exposed through the API.
import cStringIO # Exposed through the API.
import exceptions
import fnmatch
import glob
import marshal # Exposed through the API.
import optparse
import os # Somewhat exposed through the API.
import pickle # Exposed through the API.
import re # Exposed through the API.
import subprocess # Exposed through the API.
import sys # Parts exposed through API.
import tempfile # Exposed through the API.
import types
import urllib2 # Exposed through the API.
# Local imports.
# TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but
# for now it would only be a couple of functions so hardly worth it.
import gcl
import presubmit_canned_checks
# Matches key/value (or "tag") lines in changelist descriptions.
_tag_line_re = re.compile(
'^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
# Friendly names may be used for certain keys. All values for key-value pairs
# in change descriptions (like BUG=123) can be retrieved from a change object
# directly as if they were attributes, e.g. change.R (or equivalently because
# we have a friendly name for it, change.Reviewers), change.BUG (or
# change.BugIDs) and so forth.
#
# Add to this mapping as needed/desired.
SPECIAL_KEYS = {
'Reviewers' : 'R',
'BugIDs' : 'BUG',
'Tested': 'TESTED'
}
class NotImplementedException(Exception):
"""We're leaving placeholders in a bunch of places to remind us of the
design of the API, but we have not implemented all of it yet. Implement as
the need arises.
"""
pass
def normpath(path):
'''Version of os.path.normpath that also changes backward slashes to
forward slashes when not running on Windows.
'''
# This is safe to always do because the Windows version of os.path.normpath
# will replace forward slashes with backward slashes.
path = path.replace(os.sep, '/')
return os.path.normpath(path)
class OutputApi(object):
"""This class (more like a module) gets passed to presubmit scripts so that
they can specify various types of results.
"""
class PresubmitResult(object):
"""Base class for result objects."""
def __init__(self, message, items=None, long_text=''):
"""
message: A short one-line message to indicate errors.
items: A list of short strings to indicate where errors occurred.
long_text: multi-line text output, e.g. from another tool
"""
self._message = message
self._items = []
if items:
self._items = items
self._long_text = long_text.rstrip()
def _Handle(self, output_stream, input_stream, may_prompt=True):
"""Writes this result to the output stream.
Args:
output_stream: Where to write
Returns:
True if execution may continue, False otherwise.
"""
output_stream.write(self._message)
output_stream.write('\n')
for item in self._items:
output_stream.write(' %s\n' % item)
if self._long_text:
output_stream.write('\n***************\n%s\n***************\n\n' %
self._long_text)
if self.ShouldPrompt() and may_prompt:
output_stream.write('Are you sure you want to continue? (y/N): ')
response = input_stream.readline()
if response.strip().lower() != 'y':
return False
return not self.IsFatal()
def IsFatal(self):
"""An error that is fatal stops g4 mail/submit immediately, i.e. before
other presubmit scripts are run.
"""
return False
def ShouldPrompt(self):
"""Whether this presubmit result should result in a prompt warning."""
return False
class PresubmitError(PresubmitResult):
"""A hard presubmit error."""
def IsFatal(self):
return True
class PresubmitPromptWarning(PresubmitResult):
"""An warning that prompts the user if they want to continue."""
def ShouldPrompt(self):
return True
class PresubmitNotifyResult(PresubmitResult):
"""Just print something to the screen -- but it's not even a warning."""
pass
class MailTextResult(PresubmitResult):
"""A warning that should be included in the review request email."""
def __init__(self, *args, **kwargs):
raise NotImplementedException() # TODO(joi) Implement.
class InputApi(object):
"""An instance of this object is passed to presubmit scripts so they can
know stuff about the change they're looking at.
"""
def __init__(self, change, presubmit_path):
"""Builds an InputApi object.
Args:
change: A presubmit.GclChange object.
presubmit_path: The path to the presubmit script being processed.
"""
self.change = change
# We expose various modules and functions as attributes of the input_api
# so that presubmit scripts don't have to import them.
self.basename = os.path.basename
self.cPickle = cPickle
self.cStringIO = cStringIO
self.os_path = os.path
self.pickle = pickle
self.marshal = marshal
self.re = re
self.subprocess = subprocess
self.tempfile = tempfile
self.urllib2 = urllib2
# InputApi.platform is the platform you're currently running on.
self.platform = sys.platform
# The local path of the currently-being-processed presubmit script.
self.current_presubmit_path = presubmit_path
# We carry the canned checks so presubmit scripts can easily use them.
self.canned_checks = presubmit_canned_checks
def PresubmitLocalPath(self):
"""Returns the local path of the presubmit script currently being run.
This is useful if you don't want to hard-code absolute paths in the
presubmit script. For example, It can be used to find another file
relative to the PRESUBMIT.py script, so the whole tree can be branched and
the presubmit script still works, without editing its content.
"""
return self.current_presubmit_path
@staticmethod
def DepotToLocalPath(depot_path):
"""Translate a depot path to a local path (relative to client root).
Args:
Depot path as a string.
Returns:
The local path of the depot path under the user's current client, or None
if the file is not mapped.
Remember to check for the None case and show an appropriate error!
"""
local_path = gcl.GetSVNFileInfo(depot_path).get('Path')
if not local_path:
return None
else:
return local_path
@staticmethod
def LocalToDepotPath(local_path):
"""Translate a local path to a depot path.
Args:
Local path (relative to current directory, or absolute) as a string.
Returns:
The depot path (SVN URL) of the file if mapped, otherwise None.
"""
depot_path = gcl.GetSVNFileInfo(local_path).get('URL')
if not depot_path:
return None
else:
return depot_path
@staticmethod
def FilterTextFiles(affected_files, include_deletes=True):
"""Filters out all except text files and optionally also filters out
deleted files.
Args:
affected_files: List of AffectedFiles objects.
include_deletes: If false, deleted files will be filtered out.
Returns:
Filtered list of AffectedFiles objects.
"""
output_files = []
for af in affected_files:
if include_deletes or af.Action() != 'D':
path = af.AbsoluteLocalPath()
mime_type = gcl.GetSVNFileProperty(path, 'svn:mime-type')
if not mime_type or mime_type.startswith('text/'):
output_files.append(af)
return output_files
def AffectedFiles(self, include_dirs=False, include_deletes=True):
"""Same as input_api.change.AffectedFiles() except only lists files
(and optionally directories) in the same directory as the current presubmit
script, or subdirectories thereof.
"""
output_files = []
dir_with_slash = normpath(
"%s/" % os.path.dirname(self.current_presubmit_path))
if len(dir_with_slash) == 1:
dir_with_slash = ''
for af in self.change.AffectedFiles(include_dirs, include_deletes):
af_path = normpath(af.LocalPath())
if af_path.startswith(dir_with_slash):
output_files.append(af)
return output_files
def LocalPaths(self, include_dirs=False):
"""Returns local paths of input_api.AffectedFiles()."""
return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
def AbsoluteLocalPaths(self, include_dirs=False):
"""Returns absolute local paths of input_api.AffectedFiles()."""
return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
def ServerPaths(self, include_dirs=False):
"""Returns server paths of input_api.AffectedFiles()."""
return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
def AffectedTextFiles(self, include_deletes=True):
"""Same as input_api.change.AffectedTextFiles() except only lists files
in the same directory as the current presubmit script, or subdirectories
thereof.
Warning: This function retrieves the svn property on each file so it can be
slow for large change lists.
"""
return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
include_deletes)
def RightHandSideLines(self):
"""An iterator over all text lines in "new" version of changed files.
Only lists lines from new or modified text files in the change that are
contained by the directory of the currently executing presubmit script.
This is useful for doing line-by-line regex checks, like checking for
trailing whitespace.
Yields:
a 3 tuple:
the AffectedFile instance of the current file;
integer line number (1-based); and
the contents of the line as a string.
"""
return InputApi._RightHandSideLinesImpl(
self.AffectedTextFiles(include_deletes=False))
@staticmethod
def _RightHandSideLinesImpl(affected_files):
"""Implements RightHandSideLines for InputApi and GclChange."""
for af in affected_files:
lines = af.NewContents()
line_number = 0
for line in lines:
line_number += 1
yield (af, line_number, line)
class AffectedFile(object):
"""Representation of a file in a change."""
def __init__(self, path, action, repository_root=''):
self.path = path
self.action = action.strip()
self.repository_root = repository_root
def ServerPath(self):
"""Returns a path string that identifies the file in the SCM system.
Returns the empty string if the file does not exist in SCM.
"""
return gcl.GetSVNFileInfo(self.AbsoluteLocalPath()).get('URL', '')
def LocalPath(self):
"""Returns the path of this file on the local disk relative to client root.
"""
return normpath(self.path)
def AbsoluteLocalPath(self):
"""Returns the absolute path of this file on the local disk.
"""
return normpath(os.path.join(self.repository_root, self.LocalPath()))
def IsDirectory(self):
"""Returns true if this object is a directory."""
if os.path.exists(self.path):
# Retrieve directly from the file system; it is much faster than querying
# subversion, especially on Windows.
return os.path.isdir(self.path)
else:
return gcl.GetSVNFileInfo(self.path).get('Node Kind') == 'directory'
def SvnProperty(self, property_name):
"""Returns the specified SVN property of this file, or the empty string
if no such property.
"""
return gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), property_name)
def Action(self):
"""Returns the action on this opened file, e.g. A, M, D, etc."""
return self.action
def NewContents(self):
"""Returns an iterator over the lines in the new version of file.
The new version is the file in the user's workspace, i.e. the "right hand
side".
Contents will be empty if the file is a directory or does not exist.
"""
if self.IsDirectory():
return []
else:
return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines()
def OldContents(self):
"""Returns an iterator over the lines in the old version of file.
The old version is the file in depot, i.e. the "left hand side".
"""
raise NotImplementedError() # Implement when needed
def OldFileTempPath(self):
"""Returns the path on local disk where the old contents resides.
The old version is the file in depot, i.e. the "left hand side".
This is a read-only cached copy of the old contents. *DO NOT* try to
modify this file.
"""
raise NotImplementedError() # Implement if/when needed.
class GclChange(object):
"""A gcl change. See gcl.ChangeInfo for more info."""
def __init__(self, change_info, repository_root=''):
self.name = change_info.name
self.full_description = change_info.description
self.repository_root = repository_root
# From the description text, build up a dictionary of key/value pairs
# plus the description minus all key/value or "tag" lines.
self.description_without_tags = []
self.tags = {}
for line in change_info.description.splitlines():
m = _tag_line_re.match(line)
if m:
self.tags[m.group('key')] = m.group('value')
else:
self.description_without_tags.append(line)
# Change back to text and remove whitespace at end.
self.description_without_tags = '\n'.join(self.description_without_tags)
self.description_without_tags = self.description_without_tags.rstrip()
self.affected_files = [AffectedFile(info[1], info[0], repository_root) for
info in change_info.files]
def Change(self):
"""Returns the change name."""
return self.name
def Changelist(self):
"""Synonym for Change()."""
return self.Change()
def DescriptionText(self):
"""Returns the user-entered changelist description, minus tags.
Any line in the user-provided description starting with e.g. "FOO="
(whitespace permitted before and around) is considered a tag line. Such
lines are stripped out of the description this function returns.
"""
return self.description_without_tags
def FullDescriptionText(self):
"""Returns the complete changelist description including tags."""
return self.full_description
def RepositoryRoot(self):
"""Returns the repository root for this change, as an absolute path."""
return self.repository_root
def __getattr__(self, attr):
"""Return keys directly as attributes on the object.
You may use a friendly name (from SPECIAL_KEYS) or the actual name of
the key.
"""
if attr in SPECIAL_KEYS:
key = SPECIAL_KEYS[attr]
if key in self.tags:
return self.tags[key]
if attr in self.tags:
return self.tags[attr]
def AffectedFiles(self, include_dirs=False, include_deletes=True):
"""Returns a list of AffectedFile instances for all files in the change.
Args:
include_deletes: If false, deleted files will be filtered out.
include_dirs: True to include directories in the list
Returns:
[AffectedFile(path, action), AffectedFile(path, action)]
"""
if include_dirs:
affected = self.affected_files
else:
affected = filter(lambda x: not x.IsDirectory(), self.affected_files)
if include_deletes:
return affected
else:
return filter(lambda x: x.Action() != 'D', affected)
def AffectedTextFiles(self, include_deletes=True):
"""Return a list of the text files in a change.
It's common to want to iterate over only the text files.
Args:
include_deletes: Controls whether to return files with "delete" actions,
which commonly aren't relevant to presubmit scripts.
"""
return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
include_deletes)
def LocalPaths(self, include_dirs=False):
"""Convenience function."""
return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
def AbsoluteLocalPaths(self, include_dirs=False):
"""Convenience function."""
return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
def ServerPaths(self, include_dirs=False):
"""Convenience function."""
return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
def RightHandSideLines(self):
"""An iterator over all text lines in "new" version of changed files.
Lists lines from new or modified text files in the change.
This is useful for doing line-by-line regex checks, like checking for
trailing whitespace.
Yields:
a 3 tuple:
the AffectedFile instance of the current file;
integer line number (1-based); and
the contents of the line as a string.
"""
return InputApi._RightHandSideLinesImpl(
self.AffectedTextFiles(include_deletes=False))
def ListRelevantPresubmitFiles(files):
"""Finds all presubmit files that apply to a given set of source files.
Args:
files: An iterable container containing file paths.
Return:
['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py']
"""
checked_dirs = {} # Keys are directory paths, values are ignored.
source_dirs = [os.path.dirname(f) for f in files]
presubmit_files = []
for dir in source_dirs:
while (True):
if dir in checked_dirs:
break # We've already walked up from this directory.
test_path = os.path.join(dir, 'PRESUBMIT.py')
if os.path.isfile(test_path):
presubmit_files.append(normpath(test_path))
checked_dirs[dir] = ''
if dir in ['', '.']:
break
else:
dir = os.path.dirname(dir)
return presubmit_files
class PresubmitExecuter(object):
def __init__(self, change_info, committing):
"""
Args:
change_info: The ChangeInfo object for the change.
committing: True if 'gcl commit' is running, False if 'gcl upload' is.
"""
self.change = GclChange(change_info, gcl.GetRepositoryRoot())
self.committing = committing
def ExecPresubmitScript(self, script_text, presubmit_path):
"""Executes a single presubmit script.
Args:
script_text: The text of the presubmit script.
presubmit_path: The path to the presubmit file (this will be reported via
input_api.PresubmitLocalPath()).
Return:
A list of result objects, empty if no problems.
"""
input_api = InputApi(self.change, presubmit_path)
context = {}
exec script_text in context
# These function names must change if we make substantial changes to
# the presubmit API that are not backwards compatible.
if self.committing:
function_name = 'CheckChangeOnCommit'
else:
function_name = 'CheckChangeOnUpload'
if function_name in context:
context['__args'] = (input_api, OutputApi())
result = eval(function_name + '(*__args)', context)
if not (isinstance(result, types.TupleType) or
isinstance(result, types.ListType)):
raise exceptions.RuntimeError(
'Presubmit functions must return a tuple or list')
for item in result:
if not isinstance(item, OutputApi.PresubmitResult):
raise exceptions.RuntimeError(
'All presubmit results must be of types derived from '
'output_api.PresubmitResult')
else:
result = () # no error since the script doesn't care about current event.
return result
def DoPresubmitChecks(change_info,
committing,
verbose,
output_stream,
input_stream,
default_presubmit):
"""Runs all presubmit checks that apply to the files in the change.
This finds all PRESUBMIT.py files in directories enclosing the files in the
change (up to the repository root) and calls the relevant entrypoint function
depending on whether the change is being committed or uploaded.
Prints errors, warnings and notifications. Prompts the user for warnings
when needed.
Args:
change_info: The ChangeInfo object for the change.
committing: True if 'gcl commit' is running, False if 'gcl upload' is.
verbose: Prints debug info.
output_stream: A stream to write output from presubmit tests to.
input_stream: A stream to read input from the user.
default_presubmit: A default presubmit script to execute in any case.
Return:
True if execution can continue, False if not.
"""
presubmit_files = ListRelevantPresubmitFiles(change_info.FileList())
if not presubmit_files and verbose:
print "Warning, no presubmit.py found."
results = []
executer = PresubmitExecuter(change_info, committing)
if default_presubmit:
if verbose:
print "Running default presubmit script"
results += executer.ExecPresubmitScript(default_presubmit, 'PRESUBMIT.py')
for filename in presubmit_files:
if verbose:
print "Running %s" % filename
presubmit_script = gcl.ReadFile(filename)
results += executer.ExecPresubmitScript(presubmit_script, filename)
errors = []
notifications = []
warnings = []
for result in results:
if not result.IsFatal() and not result.ShouldPrompt():
notifications.append(result)
elif result.ShouldPrompt():
warnings.append(result)
else:
errors.append(result)
error_count = 0
for name, items in (('Messages', notifications),
('Warnings', warnings),
('ERRORS', errors)):
if items:
output_stream.write('\n** Presubmit %s **\n\n' % name)
for item in items:
if not item._Handle(output_stream, input_stream,
may_prompt=False):
error_count += 1
output_stream.write('\n')
if not errors and warnings:
output_stream.write(
'There were presubmit warnings. Sure you want to continue? (y/N): ')
response = input_stream.readline()
if response.strip().lower() != 'y':
error_count += 1
return (error_count == 0)
def ScanSubDirs(mask, recursive):
if not recursive:
return [x for x in glob.glob(mask) if '.svn' not in x]
else:
results = []
for root, dirs, files in os.walk('.'):
if '.svn' in dirs:
dirs.remove('.svn')
for name in files:
if fnmatch.fnmatch(name, mask):
results.append(os.path.join(root, name))
return results
def ParseFiles(args, recursive):
files = []
for arg in args:
files.extend([('M', file) for file in ScanSubDirs(arg, recursive)])
return files
def Main(argv):
parser = optparse.OptionParser(usage="%prog [options]",
version="%prog " + str(__version__))
parser.add_option("-c", "--commit", action="store_true",
help="Use commit instead of upload checks")
parser.add_option("-r", "--recursive", action="store_true",
help="Act recursively")
parser.add_option("-v", "--verbose", action="store_true",
help="Verbose output")
options, args = parser.parse_args(argv[1:])
files = ParseFiles(args, options.recursive)
if options.verbose:
print "Found %d files." % len(files)
return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files),
options.commit,
options.verbose,
sys.stdout,
sys.stdin,
default_presubmit=None)
if __name__ == '__main__':
sys.exit(Main(sys.argv))

102
presubmit_canned_checks.py Executable file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python
# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generic presubmit checks that can be reused by other presubmit checks."""
def CheckChangeHasTestedField(input_api, output_api):
"""Requires that the changelist have a TESTED= field."""
if input_api.change.Tested:
return []
else:
return [output_api.PresubmitError("Changelist must have a TESTED= field.")]
def CheckChangeHasQaField(input_api, output_api):
"""Requires that the changelist have a QA= field."""
if input_api.change.QA:
return []
else:
return [output_api.PresubmitError("Changelist must have a QA= field.")]
def CheckDoNotSubmitInDescription(input_api, output_api):
"""Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to the CL description.
"""
keyword = 'DO NOT ' + 'SUBMIT'
if keyword in input_api.change.DescriptionText():
return [output_api.PresubmitError(
keyword + " is present in the changelist description.")]
else:
return []
def CheckDoNotSubmitInFiles(input_api, output_api):
"""Checks that the user didn't add 'DO NOT ' + 'SUBMIT' to any files."""
keyword = 'DO NOT ' + 'SUBMIT'
for f, line_num, line in input_api.RightHandSideLines():
if keyword in line:
text = 'Found ' + keyword + ' in %s, line %s' % (f.LocalPath(), line_num)
return [output_api.PresubmitError(text)]
return []
def CheckDoNotSubmit(input_api, output_api):
return (
CheckDoNotSubmitInDescription(input_api, output_api) +
CheckDoNotSubmitInFiles(input_api, output_api)
)
def CheckChangeHasNoTabs(input_api, output_api):
"""Checks that there are no tab characters in any of the text files to be
submitted.
"""
for f, line_num, line in input_api.RightHandSideLines():
if '\t' in line:
return [output_api.PresubmitError(
"Found a tab character in %s, line %s" %
(f.LocalPath(), line_num))]
return []
def CheckLongLines(input_api, output_api, maxlen=80):
"""Checks that there aren't any lines longer than maxlen characters in any of
the text files to be submitted.
"""
basename = input_api.basename
bad = []
for f, line_num, line in input_api.RightHandSideLines():
if line.endswith('\n'):
line = line[:-1]
if len(line) > maxlen:
bad.append(
'%s, line %s, %s chars' %
(basename(f.LocalPath()), line_num, len(line)))
if len(bad) == 5: # Just show the first 5 errors.
break
if bad:
msg = "Found lines longer than %s characters (first 5 shown)." % maxlen
return [output_api.PresubmitPromptWarning(msg, items=bad)]
else:
return []
def CheckTreeIsOpen(input_api, output_api, url, closed):
"""Checks that an url's content doesn't match a regexp that would mean that
the tree is closed."""
try:
connection = input_api.urllib2.urlopen(url)
status = connection.read()
connection.close()
if input_api.re.match(closed, status):
long_text = status + '\n' + url
return [output_api.PresubmitError("The tree is closed.",
long_text=long_text)]
except IOError:
pass
return []

8
profile.xml Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<Profile FormatVersion="1">
<Tools>
<Tool Filename="python" AllowIntercept="true" />
<Tool Filename="cl" AllowRemote="true" VCCompiler="true" />
<Tool Filename="link" AllowRemote="false" />
</Tools>
</Profile>

5
revert Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
base_dir=$(dirname "$0")
exec python "$base_dir/revert.py" "$@"

1
revert.bat Normal file
View File

@@ -0,0 +1 @@
@python "%~dp0revert.py" %*

284
revert.py Executable file
View File

@@ -0,0 +1,284 @@
#!/usr/bin/python
# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# Tool to quickly revert a change.
import exceptions
import optparse
import os
import sys
import xml
import gcl
import gclient
class ModifiedFile(exceptions.Exception):
pass
class NoModifiedFile(exceptions.Exception):
pass
class NoBlameList(exceptions.Exception):
pass
class OutsideOfCheckout(exceptions.Exception):
pass
def getTexts(nodelist):
"""Return a list of texts in the children of a list of DOM nodes."""
rc = []
for node in nodelist:
if node.nodeType == node.TEXT_NODE:
rc.append(node.data)
else:
rc.extend(getTexts(node.childNodes))
return rc
def RunShellXML(command, print_output=False, keys=None):
output = gcl.RunShell(command, print_output)
try:
dom = xml.dom.minidom.parseString(output)
if not keys:
return dom
result = {}
for key in keys:
result[key] = getTexts(dom.getElementsByTagName(key))
except xml.parsers.expat.ExpatError:
print "Failed to parse output:\n%s" % output
raise
return result
def UniqueFast(list):
list = [item for item in set(list)]
list.sort()
return list
def GetRepoBase():
"""Returns the repository base of the root local checkout."""
xml_data = RunShellXML(['svn', 'info', '.', '--xml'], keys=['root', 'url'])
root = xml_data['root'][0]
url = xml_data['url'][0]
if not root or not url:
raise exceptions.Exception("I'm confused by your checkout")
if not url.startswith(root):
raise exceptions.Exception("I'm confused by your checkout", url, root)
return url[len(root):] + '/'
def Revert(revisions, force=False, commit=True, send_email=True, message=None,
reviewers=None):
"""Reverts many revisions in one change list.
If force is True, it will override local modifications.
If commit is True, a commit is done after the revert.
If send_mail is True, a review email is sent.
If message is True, it is used as the change description.
reviewers overrides the blames email addresses for review email."""
# Use the oldest revision as the primary revision.
changename = "revert%d" % revisions[len(revisions)-1]
if not force and os.path.exists(gcl.GetChangelistInfoFile(changename)):
print "Error, change %s already exist." % changename
return 1
# Move to the repository root and make the revision numbers sorted in
# decreasing order.
os.chdir(gcl.GetRepositoryRoot())
revisions.sort(reverse=True)
revisions_string = ",".join([str(rev) for rev in revisions])
revisions_string_rev = ",".join([str(-rev) for rev in revisions])
repo_base = GetRepoBase()
files = []
blames = []
# Get all the modified files by the revision. We'll use this list to optimize
# the svn merge.
for revision in revisions:
log = RunShellXML(["svn", "log", "-r", str(revision), "-v", "--xml"],
keys=['path', 'author'])
for file in log['path']:
# Remove the /trunk/src/ part. The + 1 is for the last slash.
if not file.startswith(repo_base):
raise OutsideOfCheckout(file)
files.append(file[len(repo_base):])
blames.extend(log['author'])
# On Windows, we need to fix the slashes once they got the url part removed.
if sys.platform == 'win32':
# On Windows, gcl expect the correct slashes.
files = [file.replace('/', os.sep) for file in files]
# Keep unique.
files = UniqueFast(files)
blames = UniqueFast(blames)
if not reviewers:
reviewers = blames
else:
reviewers = UniqueFast(reviewers)
# Make sure there's something to revert.
if not files:
raise NoModifiedFile
if not reviewers:
raise NoBlameList
if blames:
print "Blaming %s\n" % ",".join(blames)
if reviewers != blames:
print "Emailing %s\n" % ",".join(reviewers)
print "These files were modified in %s:" % revisions_string
print "\n".join(files)
print ""
# Make sure these files are unmodified with svn status.
status = gcl.RunShell(["svn", "status"] + files)
if status:
if force:
# TODO(maruel): Use the tool to correctly revert '?' files.
gcl.RunShell(["svn", "revert"] + files)
else:
raise ModifiedFile(status)
# svn up on each of these files
gcl.RunShell(["svn", "up"] + files)
files_status = {}
# Extract the first level subpaths. Subversion seems to degrade
# exponentially w.r.t. repository size during merges. Working at the root
# directory is too rough for svn due to the repository size.
roots = UniqueFast([file.split(os.sep)[0] for file in files])
for root in roots:
# Is it a subdirectory or a files?
is_root_subdir = os.path.isdir(root)
need_to_update = False
if is_root_subdir:
os.chdir(root)
file_list = []
# List the file directly since it is faster when there is only one file.
for file in files:
if file.startswith(root):
file_list.append(file[len(root)+1:])
if len(file_list) > 1:
# Listing multiple files is not supported by svn merge.
file_list = ['.']
need_to_update = True
else:
# Oops, root was in fact a file in the root directory.
file_list = [root]
root = "."
print "Reverting %s in %s/" % (revisions_string, root)
if need_to_update:
# Make sure '.' revision is high enough otherwise merge will be
# unhappy.
retcode = gcl.RunShellWithReturnCode(['svn', 'up', '.', '-N'])[1]
if retcode:
print 'svn up . -N failed in %s/.' % root
return retcode
command = ["svn", "merge", "-c", revisions_string_rev]
command.extend(file_list)
(output, retcode) = gcl.RunShellWithReturnCode(command, print_output=True)
if retcode:
print "'%s' failed:" % command
return retcode
# Grab the status
lines = output.split('\n')
for line in lines:
if line.startswith('---'):
continue
if line.startswith('Skipped'):
print ""
raise ModifiedFile(line[9:-1])
# Update the status.
status = line[:5] + ' '
file = line[5:]
if is_root_subdir:
files_status[root + os.sep + file] = status
else:
files_status[file] = status
if is_root_subdir:
os.chdir('..')
# Transform files_status from a dictionary to a list of tuple.
files_status = [(files_status[file], file) for file in files]
description = "Reverting %s." % revisions_string
if message:
description += "\n\n"
description += message
# Don't use gcl.Change() since it prompts the user for infos.
change_info = gcl.ChangeInfo(name=changename, issue='',
description=description, files=files_status)
change_info.Save()
upload_args = ['-r', ",".join(reviewers)]
if send_email:
upload_args.append('--send_mail')
if commit:
upload_args.append('--no_try')
gcl.UploadCL(change_info, upload_args)
retcode = 0
if commit:
gcl.Commit(change_info, ['--force'])
# TODO(maruel): gclient sync (to leave the local checkout in an usable
# state)
retcode = gclient.Main(["gclient.py", "sync"])
return retcode
def Main(argv):
usage = (
"""%prog [options] [revision numbers to revert]
Revert a set of revisions, send the review to Rietveld, sends a review email
and optionally commit the revert.""")
parser = optparse.OptionParser(usage=usage)
parser.add_option("-c", "--commit", default=False, action="store_true",
help="Commits right away.")
parser.add_option("-f", "--force", default=False, action="store_true",
help="Forces the local modification even if a file is "
"already modified locally.")
parser.add_option("-n", "--no_email", default=False, action="store_true",
help="Inhibits from sending a review email.")
parser.add_option("-m", "--message", default=None,
help="Additional change description message.")
parser.add_option("-r", "--reviewers", action="append",
help="Reviewers to send the email to. By default, the list "
"of commiters is used.")
options, args = parser.parse_args(argv)
revisions = []
try:
for item in args[1:]:
revisions.append(int(item))
except ValueError:
parser.error("You need to pass revision numbers.")
if not revisions:
parser.error("You need to pass revision numbers.")
retcode = 1
try:
if not os.path.exists(gcl.GetInfoDir()):
os.mkdir(gcl.GetInfoDir())
retcode = Revert(revisions, options.force, options.commit,
not options.no_email, options.message, options.reviewers)
except NoBlameList:
print "Error: no one to blame."
except NoModifiedFile:
print "Error: no files to revert."
except ModifiedFile, e:
print "You need to revert these files since they were already modified:"
print "".join(e.args)
print "You can use the --force flag to revert the files."
except OutsideOfCheckout, e:
print "Your repository doesn't contain ", str(e)
return retcode
if __name__ == "__main__":
sys.exit(Main(sys.argv))

41
tests/abandon.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Check that abandoning a branch also abandons its issue.
set -e
. ./test-lib.sh
setup_initsvn
setup_gitsvn
(
set -e
cd git-svn
git config rietveld.server localhost:8080
# Create a branch and give it an issue.
git checkout -q -b abandoned
echo "some work done on a branch" >> test
git add test; git commit -q -m "branch work"
export EDITOR=/bin/true
test_expect_success "upload succeeds" \
"$GIT_CL upload -m test master... | grep -q 'Issue created'"
# Switch back to master, delete the branch.
git checkout master
git branch -D abandoned
# Verify that "status" doesn't know about it anymore.
# The "exit" trickiness is inverting the exit status of grep.
test_expect_success "git-cl status dropped abandoned branch" \
"$GIT_CL status | grep -q abandoned && exit 1 || exit 0"
)
SUCCESS=$?
cleanup
if [ $SUCCESS == 0 ]; then
echo PASS
fi

62
tests/basic.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
set -e
. ./test-lib.sh
setup_initsvn
setup_gitsvn
(
set -e
cd git-svn
git checkout -q -b work
echo "some work done on a branch" >> test
git add test; git commit -q -m "branch work"
echo "some other work done on a branch" >> test
git add test; git commit -q -m "branch work"
test_expect_success "git-cl upload wants a server" \
"$GIT_CL upload 2>&1 | grep -q 'You must configure'"
git config rietveld.server localhost:8080
test_expect_success "git-cl status has no issue" \
"$GIT_CL status | grep -q 'no issue'"
# Prevent the editor from coming up when you upload.
export EDITOR=/bin/true
test_expect_success "upload succeeds (needs a server running on localhost)" \
"$GIT_CL upload -m test master... | grep -q 'Issue created'"
test_expect_success "git-cl status now knows the issue" \
"$GIT_CL status | grep -q 'Issue number'"
# Push a description to this URL.
URL=$($GIT_CL status | sed -ne '/Issue number/s/[^(]*(\(.*\))/\1/p')
curl --cookie dev_appserver_login="test@example.com:False" \
--data-urlencode subject="test" \
--data-urlencode description="foo-quux" \
$URL/edit
test_expect_success "git-cl dcommits ok" \
"$GIT_CL dcommit -f"
git checkout -q master
git svn -q rebase >/dev/null 2>&1
test_expect_success "dcommitted code has proper description" \
"git show | grep -q 'foo-quux'"
test_expect_success "issue no longer has a branch" \
"git cl status | grep -q 'work: None'"
test_expect_success "upstream svn has our commit" \
"svn log $REPO_URL 2>/dev/null | grep -q 'foo-quux'"
)
SUCCESS=$?
cleanup
if [ $SUCCESS == 0 ]; then
echo PASS
fi

699
tests/presubmit_unittest.py Executable file
View File

@@ -0,0 +1,699 @@
#!/usr/bin/python
# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for presubmit.py and presubmit_canned_checks.py."""
import os
import StringIO
import unittest
# Local imports
import gcl
import presubmit
import presubmit_canned_checks
class PresubmitTestsBase(unittest.TestCase):
"""Setups and tear downs the mocks but doesn't test anything as-is."""
def setUp(self):
self.original_IsFile = os.path.isfile
def MockIsFile(f):
dir = os.path.dirname(f)
return dir.endswith('haspresubmit') or dir == ''
os.path.isfile = MockIsFile
self.original_GetSVNFileInfo = gcl.GetSVNFileInfo
def MockGetSVNFileInfo(path):
if path.count('notfound'):
return {}
results = {
'Path': path[len('svn:/foo/'):],
'URL': 'svn:/foo/%s' % path.replace('\\', '/'),
}
if path.endswith('isdir'):
results['Node Kind'] = 'directory'
else:
results['Node Kind'] = 'file'
return results
gcl.GetSVNFileInfo = MockGetSVNFileInfo
self.original_GetSVNFileProperty = gcl.GetSVNFileProperty
def MockGetSVNFileProperty(path, property_name):
if property_name == 'svn:secret-property':
return 'secret-property-value'
elif path.count('binary'):
return 'application/octet-stream'
else:
if len(path) % 2:
return 'text/plain'
else:
return ''
gcl.GetSVNFileProperty = MockGetSVNFileProperty
self.original_ReadFile = gcl.ReadFile
def MockReadFile(path):
if path.count('nosuchfile'):
return None
elif path.endswith('isdir'):
self.fail('Should not attempt to read file that is directory.')
elif path.endswith('PRESUBMIT.py'):
# used in testDoPresubmitChecks
return """
def CheckChangeOnUpload(input_api, output_api):
if not input_api.change.NOSUCHKEY:
return [output_api.PresubmitError("!!")]
elif not input_api.change.REALLYNOSUCHKEY:
return [output_api.PresubmitPromptWarning("??")]
elif not input_api.change.REALLYABSOLUTELYNOSUCHKEY:
return [output_api.PresubmitPromptWarning("??"),
output_api.PresubmitError("XX!!XX")]
else:
return ()
"""
else:
return 'one:%s\r\ntwo:%s' % (path, path)
gcl.ReadFile = MockReadFile
self.original_GetRepositoryRoot = gcl.GetRepositoryRoot
def MockGetRepositoryRoot():
return ''
gcl.GetRepositoryRoot = MockGetRepositoryRoot
def tearDown(self):
os.path.isfile = self.original_IsFile
gcl.GetSVNFileInfo = self.original_GetSVNFileInfo
gcl.GetSVNFileProperty = self.original_GetSVNFileProperty
gcl.ReadFile = self.original_ReadFile
gcl.GetRepositoryRoot = self.original_GetRepositoryRoot
@staticmethod
def MakeBasicChange(name, description):
ci = gcl.ChangeInfo(name=name,
description=description,
files=[])
change = presubmit.GclChange(ci)
return change
def compareMembers(self, object, members):
"""If you add a member, be sure to add the relevant test!"""
# Skip over members starting with '_' since they are usually not meant to
# be for public use.
actual_members = [x for x in sorted(dir(object))
if not x.startswith('_')]
self.assertEqual(actual_members, sorted(members))
class PresubmitUnittest(PresubmitTestsBase):
"""General presubmit.py tests (excluding InputApi and OutputApi)."""
def testMembersChanged(self):
members = [
'AffectedFile', 'DoPresubmitChecks', 'GclChange', 'InputApi',
'ListRelevantPresubmitFiles', 'Main', 'NotImplementedException',
'OutputApi', 'ParseFiles', 'PresubmitExecuter', 'SPECIAL_KEYS',
'ScanSubDirs', 'cPickle', 'cStringIO', 'exceptions',
'fnmatch', 'gcl', 'glob', 'marshal', 'normpath', 'optparse', 'os',
'pickle', 'presubmit_canned_checks', 're', 'subprocess', 'sys',
'tempfile', 'types', 'urllib2',
]
# If this test fails, you should add the relevant test.
self.compareMembers(presubmit, members)
def testListRelevantPresubmitFiles(self):
presubmit_files = presubmit.ListRelevantPresubmitFiles([
'blat.cc',
'foo/haspresubmit/yodle/smart.h',
'moo/mat/gat/yo.h',
'foo/luck.h'])
self.failUnless(len(presubmit_files) == 2)
self.failUnless(presubmit.normpath('PRESUBMIT.py') in presubmit_files)
self.failUnless(presubmit.normpath('foo/haspresubmit/PRESUBMIT.py') in
presubmit_files)
def testTagLineRe(self):
m = presubmit._tag_line_re.match(' BUG =1223, 1445 \t')
self.failUnless(m)
self.failUnlessEqual(m.group('key'), 'BUG')
self.failUnlessEqual(m.group('value'), '1223, 1445')
def testGclChange(self):
description_lines = ('Hello there',
'this is a change',
'BUG=123',
' STORY =http://foo/ \t',
'and some more regular text \t')
files = [
['A', 'foo/blat.cc'],
['M', 'binary.dll'], # a binary file
['A', 'isdir'], # a directory
['M', 'flop/notfound.txt'], # not found in SVN, still exists locally
['D', 'boo/flap.h'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
change = presubmit.GclChange(ci)
self.failUnless(change.Change() == 'mychange')
self.failUnless(change.Changelist() == 'mychange')
self.failUnless(change.DescriptionText() ==
'Hello there\nthis is a change\nand some more regular text')
self.failUnless(change.FullDescriptionText() ==
'\n'.join(description_lines))
self.failUnless(change.BugIDs == '123')
self.failUnless(change.BUG == '123')
self.failUnless(change.STORY == 'http://foo/')
self.failUnless(len(change.AffectedFiles()) == 4)
self.failUnless(len(change.AffectedFiles(include_dirs=True)) == 5)
self.failUnless(len(change.AffectedFiles(include_deletes=False)) == 3)
self.failUnless(len(change.AffectedFiles(include_dirs=True,
include_deletes=False)) == 4)
affected_text_files = change.AffectedTextFiles(include_deletes=True)
self.failUnless(len(affected_text_files) == 3)
self.failIf(filter(lambda x: x.LocalPath() == 'binary.dll',
affected_text_files))
local_paths = change.LocalPaths()
expected_paths = [presubmit.normpath(f[1]) for f in files]
self.failUnless(
len(filter(lambda x: x in expected_paths, local_paths)) == 4)
server_paths = change.ServerPaths()
expected_paths = ['svn:/foo/%s' % f[1] for f in files if
f[1] != 'flop/notfound.txt']
expected_paths.append('') # one unknown file
self.failUnless(
len(filter(lambda x: x in expected_paths, server_paths)) == 4)
files = [[x[0], presubmit.normpath(x[1])] for x in files]
rhs_lines = []
for line in change.RightHandSideLines():
rhs_lines.append(line)
self.failUnless(rhs_lines[0][0].LocalPath() == files[0][1])
self.failUnless(rhs_lines[0][1] == 1)
self.failUnless(rhs_lines[0][2] == 'one:%s' % files[0][1])
self.failUnless(rhs_lines[1][0].LocalPath() == files[0][1])
self.failUnless(rhs_lines[1][1] == 2)
self.failUnless(rhs_lines[1][2] == 'two:%s' % files[0][1])
self.failUnless(rhs_lines[2][0].LocalPath() == files[3][1])
self.failUnless(rhs_lines[2][1] == 1)
self.failUnless(rhs_lines[2][2] == 'one:%s' % files[3][1])
self.failUnless(rhs_lines[3][0].LocalPath() == files[3][1])
self.failUnless(rhs_lines[3][1] == 2)
self.failUnless(rhs_lines[3][2] == 'two:%s' % files[3][1])
def testAffectedFile(self):
af = presubmit.AffectedFile('foo/blat.cc', 'M')
self.failUnless(af.ServerPath() == 'svn:/foo/foo/blat.cc')
self.failUnless(af.LocalPath() == presubmit.normpath('foo/blat.cc'))
self.failUnless(af.Action() == 'M')
self.failUnless(af.NewContents() == ['one:%s' % af.LocalPath(),
'two:%s' % af.LocalPath()])
af = presubmit.AffectedFile('notfound.cc', 'A')
self.failUnless(af.ServerPath() == '')
def testExecPresubmitScript(self):
description_lines = ('Hello there',
'this is a change',
'STORY=http://tracker/123')
files = [
['A', 'foo\\blat.cc'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
executer = presubmit.PresubmitExecuter(ci, False)
self.failIf(executer.ExecPresubmitScript('', 'PRESUBMIT.py'))
# No error if no on-upload entry point
self.failIf(executer.ExecPresubmitScript(
('def CheckChangeOnCommit(input_api, output_api):\n'
' return (output_api.PresubmitError("!!"))\n'),
'PRESUBMIT.py'
))
executer = presubmit.PresubmitExecuter(ci, True)
# No error if no on-commit entry point
self.failIf(executer.ExecPresubmitScript(
('def CheckChangeOnUpload(input_api, output_api):\n'
' return (output_api.PresubmitError("!!"))\n'),
'PRESUBMIT.py'
))
self.failIf(executer.ExecPresubmitScript(
('def CheckChangeOnUpload(input_api, output_api):\n'
' if not input_api.change.STORY:\n'
' return (output_api.PresubmitError("!!"))\n'
' else:\n'
' return ()'),
'PRESUBMIT.py'
))
self.failUnless(executer.ExecPresubmitScript(
('def CheckChangeOnCommit(input_api, output_api):\n'
' if not input_api.change.NOSUCHKEY:\n'
' return [output_api.PresubmitError("!!")]\n'
' else:\n'
' return ()'),
'PRESUBMIT.py'
))
try:
executer.ExecPresubmitScript(
('def CheckChangeOnCommit(input_api, output_api):\n'
' return "foo"'),
'PRESUBMIT.py')
self.fail()
except:
pass # expected case
try:
executer.ExecPresubmitScript(
('def CheckChangeOnCommit(input_api, output_api):\n'
' return ["foo"]'),
'PRESUBMIT.py')
self.fail()
except:
pass # expected case
def testDoPresubmitChecks(self):
description_lines = ('Hello there',
'this is a change',
'STORY=http://tracker/123')
files = [
['A', 'haspresubmit\\blat.cc'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
output = StringIO.StringIO()
input = StringIO.StringIO('y\n')
self.failIf(presubmit.DoPresubmitChecks(ci, False, False, output, input,
None))
self.assertEqual(output.getvalue().count('!!'), 2)
def testDoPresubmitChecksPromptsAfterWarnings(self):
description_lines = ('Hello there',
'this is a change',
'NOSUCHKEY=http://tracker/123')
files = [
['A', 'haspresubmit\\blat.cc'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
output = StringIO.StringIO()
input = StringIO.StringIO('n\n') # say no to the warning
self.failIf(presubmit.DoPresubmitChecks(ci, False, False, output, input,
None))
self.assertEqual(output.getvalue().count('??'), 2)
output = StringIO.StringIO()
input = StringIO.StringIO('y\n') # say yes to the warning
self.failUnless(presubmit.DoPresubmitChecks(ci,
False,
False,
output,
input,
None))
self.failUnless(output.getvalue().count('??'))
def testDoPresubmitChecksNoWarningPromptIfErrors(self):
description_lines = ('Hello there',
'this is a change',
'NOSUCHKEY=http://tracker/123',
'REALLYNOSUCHKEY=http://tracker/123')
files = [
['A', 'haspresubmit\\blat.cc'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
output = StringIO.StringIO()
input = StringIO.StringIO() # should be unused
self.failIf(presubmit.DoPresubmitChecks(ci, False, False, output, input,
None))
self.assertEqual(output.getvalue().count('??'), 2)
self.assertEqual(output.getvalue().count('XX!!XX'), 2)
self.assertEqual(output.getvalue().count('(y/N)'), 0)
def testDoDefaultPresubmitChecks(self):
description_lines = ('Hello there',
'this is a change',
'STORY=http://tracker/123')
files = [
['A', 'haspresubmit\\blat.cc'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
output = StringIO.StringIO()
input = StringIO.StringIO('y\n')
DEFAULT_SCRIPT = """
def CheckChangeOnUpload(input_api, output_api):
return [output_api.PresubmitError("!!")]
"""
def MockReadFile(dummy):
return ''
gcl.ReadFile = MockReadFile
def MockIsFile(dummy):
return False
os.path.isfile = MockIsFile
self.failUnless(presubmit.DoPresubmitChecks(ci, False, False, output, input,
DEFAULT_SCRIPT))
self.failIf(output.getvalue().count('!!') == 1)
def testDirectoryHandling(self):
files = [
['A', 'isdir'],
['A', 'isdir\\blat.cc'],
]
ci = gcl.ChangeInfo(name='mychange',
description='foo',
files=files)
change = presubmit.GclChange(ci)
affected_files = change.AffectedFiles(include_dirs=False)
self.failUnless(len(affected_files) == 1)
self.failUnless(affected_files[0].LocalPath().endswith('blat.cc'))
affected_files_and_dirs = change.AffectedFiles(include_dirs=True)
self.failUnless(len(affected_files_and_dirs) == 2)
def testSvnProperty(self):
affected_file = presubmit.AffectedFile('foo.cc', 'A')
self.failUnless(affected_file.SvnProperty('svn:secret-property') ==
'secret-property-value')
class InputApiUnittest(PresubmitTestsBase):
"""Tests presubmit.InputApi."""
def testMembersChanged(self):
members = [
'AbsoluteLocalPaths', 'AffectedFiles', 'AffectedTextFiles',
'DepotToLocalPath', 'FilterTextFiles', 'LocalPaths', 'LocalToDepotPath',
'PresubmitLocalPath', 'RightHandSideLines', 'ServerPaths',
'basename', 'cPickle', 'cStringIO', 'canned_checks', 'change',
'current_presubmit_path', 'marshal', 'os_path', 'pickle', 'platform',
're', 'subprocess', 'tempfile', 'urllib2',
]
# If this test fails, you should add the relevant test.
self.compareMembers(presubmit.InputApi(None, None), members)
def testDepotToLocalPath(self):
path = presubmit.InputApi.DepotToLocalPath('svn:/foo/smurf')
self.failUnless(path == 'smurf')
path = presubmit.InputApi.DepotToLocalPath('svn:/foo/notfound/burp')
self.failUnless(path == None)
def testLocalToDepotPath(self):
path = presubmit.InputApi.LocalToDepotPath('smurf')
self.failUnless(path == 'svn:/foo/smurf')
path = presubmit.InputApi.LocalToDepotPath('notfound-food')
self.failUnless(path == None)
def testInputApiConstruction(self):
# Fudge the change object, it's not used during construction anyway
api = presubmit.InputApi(change=42, presubmit_path='foo/path')
self.failUnless(api.PresubmitLocalPath() == 'foo/path')
self.failUnless(api.change == 42)
def testFilterTextFiles(self):
class MockAffectedFile(object):
def __init__(self, path, action):
self.path = path
self.action = action
def Action(self):
return self.action
def LocalPath(self):
return self.path
def AbsoluteLocalPath(self):
return self.path
list = [MockAffectedFile('foo/blat.txt', 'M'),
MockAffectedFile('foo/binary.blob', 'M'),
MockAffectedFile('blat/flop.txt', 'D')]
output = presubmit.InputApi.FilterTextFiles(list, include_deletes=True)
self.failUnless(len(output) == 2)
self.failUnless(list[0] in output and list[2] in output)
output = presubmit.InputApi.FilterTextFiles(list, include_deletes=False)
self.failUnless(len(output) == 1)
self.failUnless(list[0] in output)
def testInputApiPresubmitScriptFiltering(self):
description_lines = ('Hello there',
'this is a change',
'BUG=123',
' STORY =http://foo/ \t',
'and some more regular text')
files = [
['A', os.path.join('foo', 'blat.cc')],
['M', os.path.join('foo', 'blat', 'binary.dll')],
['D', 'foo/mat/beingdeleted.txt'],
['M', 'flop/notfound.txt'],
['A', 'boo/flap.h'],
]
ci = gcl.ChangeInfo(name='mychange',
description='\n'.join(description_lines),
files=files)
change = presubmit.GclChange(ci)
api = presubmit.InputApi(change, 'foo/PRESUBMIT.py')
affected_files = api.AffectedFiles()
self.failUnless(len(affected_files) == 3)
self.failUnless(affected_files[0].LocalPath() ==
presubmit.normpath('foo/blat.cc'))
self.failUnless(affected_files[1].LocalPath() ==
presubmit.normpath('foo/blat/binary.dll'))
self.failUnless(affected_files[2].LocalPath() ==
presubmit.normpath('foo/mat/beingdeleted.txt'))
rhs_lines = []
for line in api.RightHandSideLines():
rhs_lines.append(line)
self.failUnless(len(rhs_lines) == 2)
self.failUnless(rhs_lines[0][0].LocalPath() ==
presubmit.normpath('foo/blat.cc'))
def testGetAbsoluteLocalPath(self):
# Regression test for bug of presubmit stuff that relies on invoking
# SVN (e.g. to get mime type of file) not working unless gcl invoked
# from the client root (e.g. if you were at 'src' and did 'cd base' before
# invoking 'gcl upload' it would fail because svn wouldn't find the files
# the presubmit script was asking about).
files = [
['A', 'isdir'],
['A', os.path.join('isdir', 'blat.cc')]
]
ci = gcl.ChangeInfo(name='mychange',
description='',
files=files)
# It doesn't make sense on non-Windows platform. This is somewhat hacky,
# but it is needed since we can't just use os.path.join('c:', 'temp').
change = presubmit.GclChange(ci, 'c:' + os.sep + 'temp')
affected_files = change.AffectedFiles(include_dirs=True)
# Local paths should remain the same
self.failUnless(affected_files[0].LocalPath() ==
presubmit.normpath('isdir'))
self.failUnless(affected_files[1].LocalPath() ==
presubmit.normpath('isdir/blat.cc'))
# Absolute paths should be prefixed
self.failUnless(affected_files[0].AbsoluteLocalPath() ==
presubmit.normpath('c:/temp/isdir'))
self.failUnless(affected_files[1].AbsoluteLocalPath() ==
presubmit.normpath('c:/temp/isdir/blat.cc'))
# New helper functions need to work
absolute_paths_from_change = change.AbsoluteLocalPaths(include_dirs=True)
api = presubmit.InputApi(change=change, presubmit_path='isdir/PRESUBMIT.py')
absolute_paths_from_api = api.AbsoluteLocalPaths(include_dirs=True)
for absolute_paths in [absolute_paths_from_change,
absolute_paths_from_api]:
self.failUnless(absolute_paths[0] == presubmit.normpath('c:/temp/isdir'))
self.failUnless(absolute_paths[1] ==
presubmit.normpath('c:/temp/isdir/blat.cc'))
class OuputApiUnittest(PresubmitTestsBase):
"""Tests presubmit.OutputApi."""
def testMembersChanged(self):
members = [
'MailTextResult', 'PresubmitError', 'PresubmitNotifyResult',
'PresubmitPromptWarning', 'PresubmitResult',
]
# If this test fails, you should add the relevant test.
self.compareMembers(presubmit.OutputApi(), members)
def testOutputApiBasics(self):
self.failUnless(presubmit.OutputApi.PresubmitError('').IsFatal())
self.failIf(presubmit.OutputApi.PresubmitError('').ShouldPrompt())
self.failIf(presubmit.OutputApi.PresubmitPromptWarning('').IsFatal())
self.failUnless(
presubmit.OutputApi.PresubmitPromptWarning('').ShouldPrompt())
self.failIf(presubmit.OutputApi.PresubmitNotifyResult('').IsFatal())
self.failIf(presubmit.OutputApi.PresubmitNotifyResult('').ShouldPrompt())
# TODO(joi) Test MailTextResult once implemented.
def testOutputApiHandling(self):
output = StringIO.StringIO()
unused_input = StringIO.StringIO()
error = presubmit.OutputApi.PresubmitError('!!!')
self.failIf(error._Handle(output, unused_input))
self.failUnless(output.getvalue().count('!!!'))
output = StringIO.StringIO()
notify = presubmit.OutputApi.PresubmitNotifyResult('?see?')
self.failUnless(notify._Handle(output, unused_input))
self.failUnless(output.getvalue().count('?see?'))
output = StringIO.StringIO()
input = StringIO.StringIO('y')
warning = presubmit.OutputApi.PresubmitPromptWarning('???')
self.failUnless(warning._Handle(output, input))
self.failUnless(output.getvalue().count('???'))
output = StringIO.StringIO()
input = StringIO.StringIO('n')
warning = presubmit.OutputApi.PresubmitPromptWarning('???')
self.failIf(warning._Handle(output, input))
self.failUnless(output.getvalue().count('???'))
output = StringIO.StringIO()
input = StringIO.StringIO('\n')
warning = presubmit.OutputApi.PresubmitPromptWarning('???')
self.failIf(warning._Handle(output, input))
self.failUnless(output.getvalue().count('???'))
class CannedChecksUnittest(PresubmitTestsBase):
"""Tests presubmit_canned_checks.py."""
class MockInputApi(object):
class MockUrllib2(object):
class urlopen(object):
def __init__(self, url):
if url == 'url_to_open':
self.result = '1'
else:
self.result = '0'
def read(self):
return self.result
def close(self):
pass
def __init__(self, lines=None):
self.lines = lines
self.basename = lambda x: x
self.urllib2 = self.MockUrllib2()
self.re = presubmit.re
def RightHandSideLines(self):
for line in self.lines:
yield (presubmit.AffectedFile('bingo', 'M'), 1, line)
def testMembersChanged(self):
members = [
'CheckChangeHasNoTabs', 'CheckChangeHasQaField',
'CheckChangeHasTestedField', 'CheckDoNotSubmit',
'CheckDoNotSubmitInDescription', 'CheckDoNotSubmitInFiles',
'CheckLongLines', 'CheckTreeIsOpen',
]
# If this test fails, you should add the relevant test.
self.compareMembers(presubmit_canned_checks, members)
def testCannedCheckChangeHasTestedField(self):
change = self.MakeBasicChange('foo',
'Foo\nTESTED=did some stuff')
api = presubmit.InputApi(change, 'PRESUBMIT.py')
self.failIf(presubmit_canned_checks.CheckChangeHasTestedField(
api, presubmit.OutputApi))
change = self.MakeBasicChange('foo',
'Foo\nNEVERTESTED=did some stuff')
api = presubmit.InputApi(change, 'PRESUBMIT.py')
self.failUnless(presubmit_canned_checks.CheckChangeHasTestedField(
api, presubmit.OutputApi))
def testCannedCheckChangeHasQAField(self):
change = self.MakeBasicChange('foo',
'Foo\nQA=test floop feature very well')
api = presubmit.InputApi(change, 'PRESUBMIT.py')
self.failIf(presubmit_canned_checks.CheckChangeHasQaField(
api, presubmit.OutputApi))
change = self.MakeBasicChange('foo',
'Foo\nNOTFORQA=test floop feature very well')
api = presubmit.InputApi(change, 'PRESUBMIT.py')
self.failUnless(presubmit_canned_checks.CheckChangeHasQaField(
api, presubmit.OutputApi))
def testCannedCheckDoNotSubmitInDescription(self):
change = self.MakeBasicChange('foo', 'hello')
api = presubmit.InputApi(change, 'PRESUBMIT.py')
self.failIf(presubmit_canned_checks.CheckDoNotSubmitInDescription(
api, presubmit.OutputApi))
change = self.MakeBasicChange('foo',
'DO NOT ' + 'SUBMIT')
api = presubmit.InputApi(change, 'PRESUBMIT.py')
self.failUnless(presubmit_canned_checks.CheckDoNotSubmitInDescription(
api, presubmit.OutputApi))
def testCannedCheckDoNotSubmitInFiles(self):
self.failIf(presubmit_canned_checks.CheckDoNotSubmitInFiles(
self.MockInputApi(['hello', 'there']), presubmit.OutputApi
))
self.failUnless(presubmit_canned_checks.CheckDoNotSubmitInFiles(
self.MockInputApi(['hello', 'yo, DO NOT ' + 'SUBMIT']),
presubmit.OutputApi))
def testCannedCheckChangeHasNoTabs(self):
self.failIf(presubmit_canned_checks.CheckChangeHasNoTabs(
self.MockInputApi(['hello', 'there']), presubmit.OutputApi
))
self.failUnless(presubmit_canned_checks.CheckChangeHasNoTabs(
self.MockInputApi(['hello', 'there\tit is']), presubmit.OutputApi
))
def testCannedCheckLongLines(self):
self.failIf(presubmit_canned_checks.CheckLongLines(
self.MockInputApi(['hello', 'there']), presubmit.OutputApi, 5
))
self.failUnless(presubmit_canned_checks.CheckLongLines(
self.MockInputApi(['hello', 'there!']), presubmit.OutputApi, 5
))
def testCannedCheckTreeIsOpen(self):
self.failIf(presubmit_canned_checks.CheckTreeIsOpen(
self.MockInputApi(), presubmit.OutputApi, url='url_to_open', closed='0'
))
self.failUnless(presubmit_canned_checks.CheckTreeIsOpen(
self.MockInputApi(), presubmit.OutputApi, url='url_to_closed', closed='0'
))
if __name__ == '__main__':
unittest.main()

50
tests/test-lib.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Abort on error.
set -e
PWD=`pwd`
REPO_URL=file://$PWD/svnrepo
GIT_CL=$PWD/../git-cl
# Set up an SVN repo that has a few commits to trunk.
setup_initsvn() {
echo "Setting up test SVN repo..."
rm -rf svnrepo
svnadmin create svnrepo
rm -rf svn
svn co -q $REPO_URL svn
(
cd svn
echo "test" > test
svn add -q test
svn commit -q -m "initial commit"
echo "test2" >> test
svn commit -q -m "second commit"
)
}
# Set up a git-svn checkout of the repo.
setup_gitsvn() {
echo "Setting up test git-svn repo..."
rm -rf git-svn
# There appears to be no way to make git-svn completely shut up, so we
# redirect its output.
git svn -q clone $REPO_URL git-svn >/dev/null 2>&1
}
cleanup() {
rm -rf svnrepo svn git-svn
}
# Usage: test_expect_success "description of test" "test code".
test_expect_success() {
echo "TESTING: $1"
exit_code=0
sh -c "$2" || exit_code=$?
if [ $exit_code != 0 ]; then
echo "FAILURE: $1"
return $exit_code
fi
}

505
trychange.py Executable file
View File

@@ -0,0 +1,505 @@
#!/usr/bin/python
# Copyright (c) 2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Client-side script to send a try job to the try server. It communicates to
the try server by either writting to a svn repository or by directly connecting
to the server by HTTP.
"""
import datetime
import getpass
import logging
import optparse
import os
import shutil
import sys
import tempfile
import traceback
import urllib
import gcl
__version__ = '1.1'
# Constants
HELP_STRING = "Sorry, Tryserver is not available."
SCRIPT_PATH = os.path.join('tools', 'tryserver', 'tryserver.py')
USAGE = r"""%prog [options]
Client-side script to send a try job to the try server. It communicates to
the try server by either writting to a svn repository or by directly connecting
to the server by HTTP.
Examples:
A git patch off a web site (git inserts a/ and b/) and fix the base dir:
%prog --url http://url/to/patch.diff --patchlevel 1 --root src
Use svn to store the try job, specify an alternate email address and use a
premade diff file on the local drive:
%prog --email user@example.com
--svn_repo svn://svn.chromium.org/chrome-try/try --diff foo.diff
Running only on a 'mac' slave with revision src@123 and clobber first; specify
manually the 3 source files to use for the try job:
%prog --bot mac --revision src@123 --clobber -f src/a.cc -f src/a.h
-f include/b.h
"""
class InvalidScript(Exception):
def __str__(self):
return self.args[0] + '\n' + HELP_STRING
class NoTryServerAccess(Exception):
def __str__(self):
return self.args[0] + '\n' + HELP_STRING
def PathDifference(root, subpath):
"""Returns the difference subpath minus root."""
if subpath.find(root) != 0:
return None
# The + 1 is for the trailing / or \.
return subpath[len(root) + len(os.sep):]
def GetSourceRoot():
"""Returns the absolute directory one level up from the repository root."""
return os.path.abspath(os.path.join(gcl.GetRepositoryRoot(), '..'))
def ExecuteTryServerScript():
"""Locates the tryserver script, executes it and returns its dictionary.
The try server script contains the repository-specific try server commands."""
script_locals = {}
try:
# gcl.GetRepositoryRoot() may throw an exception.
script_path = os.path.join(gcl.GetRepositoryRoot(), SCRIPT_PATH)
except Exception:
return script_locals
if os.path.exists(script_path):
try:
exec(gcl.ReadFile(script_path), script_locals)
except Exception, e:
# TODO(maruel): Need to specialize the exception trapper.
traceback.print_exc()
raise InvalidScript('%s is invalid.' % script_path)
return script_locals
def EscapeDot(name):
return name.replace('.', '-')
def RunCommand(command):
output, retcode = gcl.RunShellWithReturnCode(command)
if retcode:
raise NoTryServerAccess(' '.join(command) + '\nOuput:\n' + output)
return output
class SCM(object):
"""Simplistic base class to implement one function: ProcessOptions."""
def __init__(self, options):
self.options = options
def ProcessOptions(self):
raise Unimplemented
class SVN(SCM):
"""Gathers the options and diff for a subversion checkout."""
def GenerateDiff(self, files, root):
"""Returns a string containing the diff for the given file list.
The files in the list should either be absolute paths or relative to the
given root. If no root directory is provided, the repository root will be
used.
"""
previous_cwd = os.getcwd()
if root is None:
os.chdir(gcl.GetRepositoryRoot())
else:
os.chdir(root)
diff = []
for file in files:
# Use svn info output instead of os.path.isdir because the latter fails
# when the file is deleted.
if gcl.GetSVNFileInfo(file).get("Node Kind") == "directory":
continue
# If the user specified a custom diff command in their svn config file,
# then it'll be used when we do svn diff, which we don't want to happen
# since we want the unified diff. Using --diff-cmd=diff doesn't always
# work, since they can have another diff executable in their path that
# gives different line endings. So we use a bogus temp directory as the
# config directory, which gets around these problems.
if sys.platform.startswith("win"):
parent_dir = tempfile.gettempdir()
else:
parent_dir = sys.path[0] # tempdir is not secure.
bogus_dir = os.path.join(parent_dir, "temp_svn_config")
if not os.path.exists(bogus_dir):
os.mkdir(bogus_dir)
# Grabs the diff data.
data = gcl.RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
# We know the diff will be incorrectly formatted. Fix it.
if gcl.IsSVNMoved(file):
# The file is "new" in the patch sense. Generate a homebrew diff.
# We can't use ReadFile() since it's not using binary mode.
file_handle = open(file, 'rb')
file_content = file_handle.read()
file_handle.close()
# Prepend '+ ' to every lines.
file_content = ['+ ' + i for i in file_content.splitlines(True)]
nb_lines = len(file_content)
# We need to use / since patch on unix will fail otherwise.
file = file.replace('\\', '/')
data = "Index: %s\n" % file
data += ("============================================================="
"======\n")
# Note: Should we use /dev/null instead?
data += "--- %s\n" % file
data += "+++ %s\n" % file
data += "@@ -0,0 +1,%d @@\n" % nb_lines
data += ''.join(file_content)
diff.append(data)
os.chdir(previous_cwd)
return "".join(diff)
def ProcessOptions(self):
if not self.options.diff:
# Generate the diff with svn and write it to the submit queue path. The
# files are relative to the repository root, but we need patches relative
# to one level up from there (i.e., 'src'), so adjust both the file
# paths and the root of the diff.
source_root = GetSourceRoot()
prefix = PathDifference(source_root, gcl.GetRepositoryRoot())
adjusted_paths = [os.path.join(prefix, x) for x in self.options.files]
self.options.diff = self.GenerateDiff(adjusted_paths, root=source_root)
class GIT(SCM):
"""Gathers the options and diff for a git checkout."""
def GenerateDiff(self):
"""Get the diff we'll send to the try server. We ignore the files list."""
branch = upload.RunShell(['git', 'cl', 'upstream']).strip()
diff = upload.RunShell(['git', 'diff-tree', '-p', '--no-prefix',
branch, 'HEAD']).splitlines(True)
for i in range(len(diff)):
# In the case of added files, replace /dev/null with the path to the
# file being added.
if diff[i].startswith('--- /dev/null'):
diff[i] = '--- %s' % diff[i+1][4:]
return ''.join(diff)
def GetEmail(self):
# TODO: check for errors here?
return upload.RunShell(['git', 'config', 'user.email']).strip()
def GetPatchName(self):
"""Construct a name for this patch."""
# TODO: perhaps include the hash of the current commit, to distinguish
# patches?
branch = upload.RunShell(['git', 'symbolic-ref', 'HEAD']).strip()
if not branch.startswith('refs/heads/'):
raise "Couldn't figure out branch name"
branch = branch[len('refs/heads/'):]
return branch
def ProcessOptions(self):
if not self.options.diff:
self.options.diff = self.GenerateDiff()
if not self.options.name:
self.options.name = self.GetPatchName()
if not self.options.email:
self.options.email = self.GetEmail()
def _ParseSendChangeOptions(options):
"""Parse common options passed to _SendChangeHTTP and _SendChangeSVN."""
values = {}
if options.email:
values['email'] = options.email
values['user'] = options.user
values['name'] = options.name
if options.bot:
values['bot'] = ','.join(options.bot)
if options.revision:
values['revision'] = options.revision
if options.clobber:
values['clobber'] = 'true'
if options.tests:
values['tests'] = ','.join(options.tests)
if options.root:
values['root'] = options.root
if options.patchlevel:
values['patchlevel'] = options.patchlevel
if options.issue:
values['issue'] = options.issue
if options.patchset:
values['patchset'] = options.patchset
return values
def _SendChangeHTTP(options):
"""Send a change to the try server using the HTTP protocol."""
script_locals = ExecuteTryServerScript()
if not options.host:
options.host = script_locals.get('try_server_http_host', None)
if not options.host:
raise NoTryServerAccess('Please use the --host option to specify the try '
'server host to connect to.')
if not options.port:
options.port = script_locals.get('try_server_http_port', None)
if not options.port:
raise NoTryServerAccess('Please use the --port option to specify the try '
'server port to connect to.')
values = _ParseSendChangeOptions(options)
values['patch'] = options.diff
url = 'http://%s:%s/send_try_patch' % (options.host, options.port)
proxies = None
if options.proxy:
if options.proxy.lower() == 'none':
# Effectively disable HTTP_PROXY or Internet settings proxy setup.
proxies = {}
else:
proxies = {'http': options.proxy, 'https': options.proxy}
try:
connection = urllib.urlopen(url, urllib.urlencode(values), proxies=proxies)
except IOError, e:
# TODO(thestig) this probably isn't quite right.
if values.get('bot') and e[2] == 'got a bad status line':
raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url)
else:
raise NoTryServerAccess('%s is unaccessible.' % url)
if not connection:
raise NoTryServerAccess('%s is unaccessible.' % url)
if connection.read() != 'OK':
raise NoTryServerAccess('%s is unaccessible.' % url)
return options.name
def _SendChangeSVN(options):
"""Send a change to the try server by committing a diff file on a subversion
server."""
script_locals = ExecuteTryServerScript()
if not options.svn_repo:
options.svn_repo = script_locals.get('try_server_svn', None)
if not options.svn_repo:
raise NoTryServerAccess('Please use the --svn_repo option to specify the'
' try server svn repository to connect to.')
values = _ParseSendChangeOptions(options)
description = ''
for (k,v) in values.iteritems():
description += "%s=%s\n" % (k,v)
# Do an empty checkout.
temp_dir = tempfile.mkdtemp()
temp_file = tempfile.NamedTemporaryFile()
temp_file_name = temp_file.name
try:
RunCommand(['svn', 'checkout', '--depth', 'empty', '--non-interactive',
options.svn_repo, temp_dir])
# TODO(maruel): Use a subdirectory per user?
current_time = str(datetime.datetime.now()).replace(':', '.')
file_name = (EscapeDot(options.user) + '.' + EscapeDot(options.name) +
'.%s.diff' % current_time)
full_path = os.path.join(temp_dir, file_name)
full_url = options.svn_repo + '/' + file_name
file_found = False
try:
RunCommand(['svn', 'ls', '--non-interactive', full_url])
file_found = True
except NoTryServerAccess:
pass
if file_found:
# The file already exists in the repo. Note that commiting a file is a
# no-op if the file's content (the diff) is not modified. This is why the
# file name contains the date and time.
RunCommand(['svn', 'update', '--non-interactive', full_path])
file = open(full_path, 'wb')
file.write(options.diff)
file.close()
else:
# Add the file to the repo
file = open(full_path, 'wb')
file.write(options.diff)
file.close()
RunCommand(["svn", "add", '--non-interactive', full_path])
temp_file.write(description)
temp_file.flush()
RunCommand(["svn", "commit", '--non-interactive', full_path, '--file',
temp_file_name])
finally:
temp_file.close()
shutil.rmtree(temp_dir, True)
return options.name
def GuessVCS(options):
"""Helper to guess the version control system.
NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't
support it yet.
This examines the current directory, guesses which SCM we're using, and
returns an instance of the appropriate class. Exit with an error if we can't
figure it out.
Returns:
A SCM instance. Exits if the SCM can't be guessed.
"""
# Subversion has a .svn in all working directories.
if os.path.isdir('.svn'):
logging.info("Guessed VCS = Subversion")
return SVN(options)
# Git has a command to test if you're in a git tree.
# Try running it, but don't die if we don't have git installed.
try:
out, returncode = gcl.RunShellWithReturnCode(["git", "rev-parse",
"--is-inside-work-tree"])
if returncode == 0:
logging.info("Guessed VCS = Git")
return GIT(options)
except OSError, (errno, message):
if errno != 2: # ENOENT -- they don't have git installed.
raise
raise NoTryServerAccess("Could not guess version control system. "
"Are you in a working copy directory?")
def TryChange(argv,
file_list,
swallow_exception,
prog=None):
# Parse argv
parser = optparse.OptionParser(usage=USAGE,
version=__version__,
prog=prog)
group = optparse.OptionGroup(parser, "Result and status")
group.add_option("-u", "--user", default=getpass.getuser(),
help="Owner user name [default: %default]")
group.add_option("-e", "--email", default=os.environ.get('EMAIL_ADDRESS'),
help="Email address where to send the results. Use the "
"EMAIL_ADDRESS environment variable to set the default "
"email address [default: %default]")
group.add_option("-n", "--name", default='Unnamed',
help="Descriptive name of the try job")
group.add_option("--issue", type='int',
help="Update rietveld issue try job status")
group.add_option("--patchset", type='int',
help="Update rietveld issue try job status")
parser.add_option_group(group)
group = optparse.OptionGroup(parser, "Try job options")
group.add_option("-b", "--bot", action="append",
help="Only use specifics build slaves, ex: '--bot win' to "
"run the try job only on the 'win' slave; see the try "
"server watefall for the slave's name")
group.add_option("-r", "--revision",
help="Revision to use for the try job; default: the "
"revision will be determined by the try server; see "
"its waterfall for more info")
group.add_option("-c", "--clobber", action="store_true",
help="Force a clobber before building; e.g. don't do an "
"incremental build")
# Override the list of tests to run, use multiple times to list many tests
# (or comma separated)
group.add_option("-t", "--tests", action="append",
help=optparse.SUPPRESS_HELP)
parser.add_option_group(group)
group = optparse.OptionGroup(parser, "Patch to run")
group.add_option("-f", "--file", default=file_list, dest="files",
metavar="FILE", action="append",
help="Use many times to list the files to include in the "
"try, relative to the repository root")
group.add_option("--diff",
help="File containing the diff to try")
group.add_option("--url",
help="Url where to grab a patch")
group.add_option("--root",
help="Root to use for the patch; base subdirectory for "
"patch created in a subdirectory")
group.add_option("--patchlevel", type='int', metavar="LEVEL",
help="Used as -pN parameter to patch")
parser.add_option_group(group)
group = optparse.OptionGroup(parser, "Access the try server by HTTP")
group.add_option("--use_http", action="store_const", const=_SendChangeHTTP,
dest="send_patch", default=_SendChangeHTTP,
help="Use HTTP to talk to the try server [default]")
group.add_option("--host",
help="Host address")
group.add_option("--port",
help="HTTP port")
group.add_option("--proxy",
help="HTTP proxy")
parser.add_option_group(group)
group = optparse.OptionGroup(parser, "Access the try server with SVN")
group.add_option("--use_svn", action="store_const", const=_SendChangeSVN,
dest="send_patch",
help="Use SVN to talk to the try server")
group.add_option("--svn_repo", metavar="SVN_URL",
help="SVN url to use to write the changes in; --use_svn is "
"implied when using --svn_repo")
parser.add_option_group(group)
options, args = parser.parse_args(argv)
# Switch the default accordingly.
if options.svn_repo:
options.send_patch = _SendChangeSVN
if len(args) == 1 and args[0] == 'help':
parser.print_help()
if (not options.files and (not options.issue and options.patchset) and
not options.diff and not options.url):
# TODO(maruel): It should just try the modified files showing up in a
# svn status.
print "Nothing to try, changelist is empty."
return
try:
# Convert options.diff into the content of the diff.
if options.url:
options.diff = urllib.urlopen(options.url).read()
elif options.diff:
options.diff = gcl.ReadFile(options.diff)
# Process the VCS in any case at least to retrieve the email address.
try:
options.scm = GuessVCS(options)
options.scm.ProcessOptions()
except NoTryServerAccess, e:
# If we got the diff, we don't care.
if not options.diff:
raise
# Send the patch.
patch_name = options.send_patch(options)
print 'Patch \'%s\' sent to try server.' % patch_name
if patch_name == 'Unnamed':
print "Note: use --name NAME to change the try's name."
except (InvalidScript, NoTryServerAccess), e:
if swallow_exception:
return
print e
if __name__ == "__main__":
TryChange(None, None, False)

1373
upload.py Executable file

File diff suppressed because it is too large Load Diff