Initial commit of git-restack

Change-Id: I5508816fcd5c6362ee17a494ae8b997de29505d3
This commit is contained in:
James E. Blair 2015-12-18 12:50:52 -08:00
parent cfb726e4a0
commit c6a59489fa
29 changed files with 583 additions and 3429 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
build build
dist dist
git_review.egg-info git_restack.egg-info
MANIFEST MANIFEST
AUTHORS AUTHORS
ChangeLog ChangeLog

View File

@ -1,4 +1,4 @@
[gerrit] [gerrit]
host=review.openstack.org host=review.openstack.org
port=29418 port=29418
project=openstack-infra/git-review.git project=openstack-infra/git-restack.git

View File

@ -2,7 +2,7 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ ./git_review/tests $LISTOPT $IDOPTION ${PYTHON:-python} -m subunit.run discover -t ./ ./git_restack/tests $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE test_id_option=--load-list $IDFILE
test_list_option=--list test_list_option=--list

View File

@ -1,15 +1,14 @@
============================ ============================
Contributing to git-review Contributing to git-restack
============================ ============================
To get the latest code, see: https://git.openstack.org/cgit/openstack-infra/git-review To get the latest code, see: https://git.openstack.org/cgit/openstack-infra/git-restack
Bugs are handled at: https://storyboard.openstack.org/#!/project/719 Bugs are handled at: https://storyboard.openstack.org/#!/project/838
There is a mailing list at: http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-infra There is a mailing list at: http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-infra
Code reviews, as you might expect, are handled by gerrit at: Code reviews are handled by gerrit at: https://review.openstack.org
https://review.openstack.org
See http://wiki.openstack.org/GerritWorkflow for details. Pull See http://wiki.openstack.org/GerritWorkflow for details. Pull
requests submitted through GitHub will be ignored. requests submitted through GitHub will be ignored.

View File

@ -1,13 +1,13 @@
Hacking git-review Hacking git-restack
================== ==================
Development of git-review is managed by OpenStack's Gerrit, which can be Development of git-restack is managed by OpenStack's Gerrit, which can be
found at https://review.openstack.org/ found at https://review.openstack.org/
Instructions on submitting patches can be found at Instructions on submitting patches can be found at
http://docs.openstack.org/infra/manual/developers.html#development-workflow http://docs.openstack.org/infra/manual/developers.html#development-workflow
git-review should, in general, not depend on a huge number of external git-restack should, in general, not depend on a huge number of external
libraries, so that installing it is a lightweight operation. libraries, so that installing it is a lightweight operation.
OpenStack Style Commandments OpenStack Style Commandments

View File

@ -3,5 +3,5 @@ include LICENSE
include AUTHORS include AUTHORS
include ChangeLog include ChangeLog
include HACKING.rst include HACKING.rst
include git-review.1 include git-restack.1
include tox.ini include tox.ini

View File

@ -1,12 +1,12 @@
git-review git-restack
========== ===========
A git command for submitting branches to Gerrit A git command for editing a series of commits without rebasing.
git-review is a tool that helps submitting git branches to gerrit for git-restack is a tool that performs an interactive git rebase of a
review. branch without changing the commit upon which the branch is based.
* Free software: Apache license * Free software: Apache license
* Documentation: http://docs.openstack.org/infra/git-review/ * Documentation: http://docs.openstack.org/infra/git-restack/
* Source: https://git.openstack.org/cgit/openstack-infra/git-review * Source: https://git.openstack.org/cgit/openstack-infra/git-restack
* Bugs: https://storyboard.openstack.org/#!/project/719 * Bugs: https://storyboard.openstack.org/#!/project/838

View File

@ -85,17 +85,17 @@ qthelp:
@echo @echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:" ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/git-review.qhcp" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/git-restack.qhcp"
@echo "To view the help file:" @echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/git-review.qhc" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/git-restack.qhc"
devhelp: devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo @echo
@echo "Build finished." @echo "Build finished."
@echo "To view the help file:" @echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/git-review" @echo "# mkdir -p $$HOME/.local/share/devhelp/git-restack"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/git-review" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/git-restack"
@echo "# devhelp" @echo "# devhelp"
epub: epub:

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# git-review documentation build configuration file, created by # git-restack documentation build configuration file, created by
# sphinx-quickstart on Mon Dec 1 14:06:22 2014. # sphinx-quickstart on Mon Dec 1 14:06:22 2014.
# #
# This file is execfile()d with the current directory set to its # This file is execfile()d with the current directory set to its
@ -46,7 +46,7 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'git-review' project = u'git-restack'
copyright = u'2014, OpenStack Contributors' copyright = u'2014, OpenStack Contributors'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
@ -170,7 +170,7 @@ html_theme = 'default'
#html_file_suffix = None #html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'git-reviewdoc' htmlhelp_basename = 'git-restackdoc'
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@ -190,7 +190,7 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
('index', 'git-review.tex', u'git-review Documentation', ('index', 'git-restack.tex', u'git-restack Documentation',
u'OpenStack Contributors', 'manual'), u'OpenStack Contributors', 'manual'),
] ]
@ -220,7 +220,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'git-review', u'git-review Documentation', ('index', 'git-restack', u'git-restack Documentation',
[u'OpenStack Contributors'], 1) [u'OpenStack Contributors'], 1)
] ]
@ -234,8 +234,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'git-review', u'git-review Documentation', ('index', 'git-restack', u'git-restack Documentation',
u'OpenStack Contributors', 'git-review', 'One line description of project.', u'OpenStack Contributors', 'git-restack', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]

View File

@ -2,18 +2,7 @@
Running tests Running tests
============= =============
To run git-restack tests the following commands may by run::
Running tests for git-review means running a local copy of Gerrit to
check that git-review interacts correctly with it. This requires the
following:
* a Java Runtime Environment on the machine to run tests on
* Internet access to download the gerrit.war file, or a locally
cached copy (it needs to be located in a .gerrit directory at the
top level of the git-review project)
To run git-review integration tests the following commands may by run::
tox -e py27 tox -e py27
tox -e py26 tox -e py26

View File

@ -1,9 +1,9 @@
============ ============
git-review git-restack
============ ============
``git-review`` is a tool that helps submitting git branches to gerrit ``git-restack`` is a tool that helps edit a series of commits without
for review. rebasing.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2

View File

@ -2,89 +2,13 @@
Installation and Configuration Installation and Configuration
================================ ================================
Installing git-review Installing git-restack
===================== =====================
Install with pip install git-review Install with pip install git-restack
For assistance installing pip on your os check out get-pip: For assistance installing pip on your os check out get-pip:
http://pip.readthedocs.org/en/latest/installing.html http://pip.readthedocs.org/en/latest/installing.html
For installation from source simply add git-review to your $PATH For installation from source simply add git-restack to your $PATH
after installing the dependencies listed in requirements.txt after installing the dependencies listed in requirements.txt
Setup
=====
By default, git-review will look for a remote named 'gerrit' for working
with Gerrit. If the remote exists, git-review will submit the current
branch to HEAD:refs/for/master at that remote.
If the Gerrit remote does not exist, git-review looks for a file
called .gitreview at the root of the repository with information about
the gerrit remote. Assuming that file is present, git-review should
be able to automatically configure your repository the first time it
is run.
The name of the Gerrit remote is configurable; see the configuration
section below.
.gitreview file format
======================
Example .gitreview file (used to upload for git-review itself)::
[gerrit]
host=review.openstack.org
port=29418
project=openstack-infra/git-review.git
defaultbranch=master
Required values: host, project
Optional values: port (default: 29418), defaultbranch (default: master),
defaultremote (default: gerrit).
**Notes**
* Username is not required because it is requested on first run
* Unlike git config files, there cannot be any whitespace before the name
of the variable.
* Upon first run, git-review will create a remote for working with Gerrit,
if it does not already exist. By default, the remote name is 'gerrit',
but this can be overridden with the 'defaultremote' configuration
option.
* You can specify different values to be used as defaults in
~/.config/git-review/git-review.conf or /etc/git-review/git-review.conf.
* Git-review will query git credential system for gerrit user/password when
authentication failed over http(s). Unlike git, git-review does not persist
gerrit user/password in git credential system for security purposes and git
credential system configuration stays under user responsibility.
Hooks
=====
git-review has a custom hook mechanism to run a script before certain
actions. This is done in the same spirit as the classic hooks in git.
There are two types of hooks, a global one which is stored in
~/.config/git-review/hooks/ and one local to the repository stored in
.git/hooks/ with the other git hook scripts.
**The script needs be executable before getting executed**
The name of the script is $action-review where action can be
:
* pre - run at first before doing anything.
* post - run at the end after the review was sent.
* draft - run when in draft mode.
if the script returns with an exit status different than zero,
git-review will exit with the a custom shell exit code 71.

View File

@ -2,54 +2,11 @@
Usage Usage
======= =======
Hack on some code, then:: To interactively rebase the current branch against the most recent
commit in common with the master branch, run::
git review git restack
If you want to submit that code to a branch other than "master", then:: If your branch is based on a different branch, run::
git review branchname git restack branchname
If you want to submit to a different remote::
git review -r my-remote
If you want to supply a review topic::
git review -t topic/awesome-feature
If you want to subscribe some reviewers::
git review --reviewers a@example.com b@example.com
If you want to disable autogenerated topic::
git review -T
If you want to submit a branch for review and then remove the local branch::
git review -f
If you want to skip the automatic "git rebase -i" step::
git review -R
If you want to download change 781 from gerrit to review it::
git review -d 781
If you want to download patchset 4 for change 781 from gerrit to review it::
git review -d 781,4
If you want to compare patchset 4 with patchset 10 of change 781 from gerrit::
git review -m 781,4-10
If you want to see a list of open reviews::
git review -l
If you just want to do the commit message and remote setup steps::
git review -s

78
git-restack.1 Normal file
View File

@ -0,0 +1,78 @@
.\" Uses mdoc(7). See `man 7 mdoc` for details about the syntax used here
.\"
.Dd December 18th, 2015
.Dt GIT\-RESTACK 1
.Sh NAME
.Nm git\-restack
.Nd Edit a series of commits without rebasing
.Sh SYNOPSIS
.Nm
.Op Ar branch
.Nm
.Fl \-version
.Sh DESCRIPTION
.Nm
performs an interactive git rebase of the current branch based on the
most recent commit in a target branch. When maintaining a large patch
series, it frequently becomes necessary to edit individual patches in
the series. Simply rebasing the series on the tip of the remote
branch has the secondary effect of changing the branch point of the
series. In some cases this may be desirable, but in others, such as
when using a code review system like Gerrit, it makes it difficult to
examine diffs between different versions of patchsets.
.Nm
will allow you to rebase the series without changing the commit the
series is based on.
.Pp
If supplied,
.Ar branch
indicates the branch this series is based on. If it is not present,
.Nm
will check git configuration or look for a
.Pa .gitreview
file and use the default branch specified there. If neither is found,
it defaults to the master branch.
.Pp
The following options are available:
.Bl -tag -width indent
.It gitreview.branch
This setting determines the default base branch
.Sh FILES
If there is a
.Pa .gitreview
file in the project,
.Nm
will use it to determine the default base branch.
The format is similar to the Windows .ini file format:
.Bd -literal -offset indent
[gerrit]
host=\fIhostname\fP
port=\fITCP port number of gerrit\fP
project=\fIproject name\fP
defaultbranch=\fIbranch to work on\fP
.Ed
.Pp
When the same option is provided through FILES and CONFIGURATION, the
CONFIGURATION value wins.
.Pp
.Sh EXAMPLES
To perform an interactive rebase against the master branch:
.Pp
.Bd -literal -offset indent
$ git\-restack
.Ed
.Pp
To perform an interactive rebase against a branch named
.Pa stable
:
.Pp
.Bd -literal -offset indent
$ git\-restack stable
.Ed
.Sh BUGS
Bug reports can be submitted to
.Lk https://storyboard.openstack.org/#!/project/838
.Sh AUTHORS
.Nm
is maintained by
.An "The OpenStack project"

View File

@ -1,458 +0,0 @@
.\" Uses mdoc(7). See `man 7 mdoc` for details about the syntax used here
.\"
.Dd June 12th, 2015
.Dt GIT\-REVIEW 1
.Sh NAME
.Nm git\-review
.Nd Submit changes to Gerrit for review
.Sh SYNOPSIS
.Nm
.Op Fl r Ar remote
.Op Fl uv
.Fl d Ar change
.Op Ar branch
.Nm
.Op Fl r Ar remote
.Op Fl uv
.Fl x Ar change
.Op Ar branch
.Nm
.Op Fl r Ar remote
.Op Fl uv
.Fl N Ar change
.Op Ar branch
.Nm
.Op Fl r Ar remote
.Op Fl uv
.Fl X Ar change
.Op Ar branch
.Nm
.Op Fl r Ar remote
.Op Fl uv
.Fl m
.Ar change\-ps\-range
.Op Ar branch
.Nm
.Op Fl r Ar remote
.Op Fl fnuv
.Fl s
.Op Ar branch
.Nm
.Op Fl fnuvDRT
.Op Fl r Ar remote
.Op Fl t Ar topic
.Op Fl \-reviewers Ar reviewer ...
.Op Ar branch
.Nm
.Fl l
.Nm
.Fl \-version
.Sh DESCRIPTION
.Nm
automates and streamlines some of the tasks involved with
submitting local changes to a Gerrit server for review. It is
designed to make it easier to comprehend Gerrit, especially for
users that have recently switched to Git from another version
control system.
.Pp
.Ar change
can be
.Ar changeNumber
as obtained using
.Fl \-list
option, or it can be
.Ar changeNumber,patchsetNumber
for fetching exact patchset from the change.
In that case local branch name will have a \-patch[patchsetNumber] suffix.
.Pp
The following options are available:
.Bl -tag -width indent
.It Fl d Ar change , Fl \-download= Ns Ar change
Download
.Ar change
from Gerrit
into a local branch. The branch will be named after the patch author and the name of a topic.
If the local branch already exists, it will attempt to update with the latest patchset for this change.
.It Fl x Ar change , Fl \-cherrypick= Ns Ar change
Apply
.Ar change
from Gerrit and commit into the current local branch ("cherry pick").
No additional branch is created.
.Pp
This makes it possible to review a change without creating a local branch for
it. On the other hand, be aware: if you are not careful, this can easily result
in additional patch sets for dependent changes. Also, if the current branch is
different enough, the change may not apply at all or produce merge conflicts
that need to be resolved by hand.
.It Fl N Ar change , Fl \-cherrypickonly= Ns Ar change
Apply
.Ar change
from Gerrit
into the current working directory, add it to the staging area ("git index"), but do not commit it.
.Pp
This makes it possible to review a change without creating a local commit for
it. Useful if you want to merge several commits into one that will be submitted for review.
.Pp
If the current branch is different enough, the change may not apply at all
or produce merge conflicts that need to be resolved by hand.
.It Fl X Ar change , Fl \-cherrypickindicate= Ns Ar change
Apply
.Ar change
from Gerrit and commit into the current local branch ("cherry pick"),
indicating which commit this change was cherry\-picked from.
.Pp
This makes it possible to re\-review a change for a different branch without
creating a local branch for it.
.Pp
If the current branch is different enough, the change may not apply at all
or produce merge conflicts that need to be resolved by hand.
.It Fl m Ar change\-ps\-range , Fl \-compare= Ns Ar change\-ps\-range
Download the specified patchsets for
.Ar change
from Gerrit, rebase both on master and display differences (git\-diff).
.Pp
.Ar change\-ps\-range
can be specified as
.Ar changeNumber, Ns Ar oldPatchSetNumber Ns Op Ns Ar \-newPatchSetNumber
.Pp
.Ar oldPatchSetNumber
is mandatory, and if
.Ar newPatchSetNumber
is not specified, the latest patchset will be used.
.Pp
This makes it possible to easily compare what has changed from last time you
reviewed the proposed change.
.Pp
If the master branch is different enough, the rebase can produce merge conflicts.
If that happens rebasing will be aborted and diff displayed for not\-rebased branches.
You can also use
.Ar \-\-no\-rebase ( Ar \-R )
to always skip rebasing.
.It Fl f , Fl \-finish
Close down the local branch and switch back to the target branch on
successful submission.
.It Fl n , Fl \-dry\-run
Don\(aqt actually perform any commands that have direct effects. Print them
instead.
.It Fl r Ar remote , Fl \-remote= Ns Ar remote
Git remote to use for Gerrit.
.It Fl s , Fl \-setup
Just run the repo setup commands but don\(aqt submit anything.
.It Fl t Ar topic , Fl \-topic= Ns Ar topic
Sets the target topic for this change on the gerrit server.
If not specified, a bug number from the commit summary will be used. Alternatively, the local branch name will be used if different from remote branch.
.It Fl T , Fl \-no\-topic
Submit review without topic.
.It Fl \-reviewers Ar reviewer ...
Subscribe one or more reviewers to the uploaded patch sets. Reviewers should be identifiable by Gerrit (usually use their Gerrit username or email address).
.It Fl u , Fl \-update
Skip cached local copies and force updates from network resources.
.It Fl l , Fl \-list
List the available reviews on the gerrit server for this project.
.It Fl y , Fl \-yes
Indicate that you do, in fact, understand if you are submitting more than
one patch.
.It Fl v Fl \-verbose
Turns on more verbose output.
.It Fl D , Fl \-draft
Submit review as a draft. Requires Gerrit 2.3 or newer.
.It Fl R , Fl \-no\-rebase
Do not automatically perform a rebase before submitting the change to
Gerrit.
.Pp
When submitting a change for review, you will usually want it to be based on the tip of upstream branch in order to avoid possible conflicts. When amending a change and rebasing the new patchset, the Gerrit web interface will show a difference between the two patchsets which contains all commits in between. This may confuse many reviewers that would expect to see a much simpler difference.
.Pp
Also can be used for
.Fl \-compare
to skip automatic rebase of fetched reviews.
.It Fl \-track
Choose the branch to submit the change against (and, if
rebasing, to rebase against) from the branch being tracked
(if a branch is being tracked), and set the tracking branch
when downloading a change to point to the remote and branch
against which patches should be submitted.
See gitreview.track configuration.
.It Fl \-no\-track
Ignore any branch being tracked by the current branch,
overriding gitreview.track.
This option is implied by providing a specific branch name
on the command line.
.It Fl \-version
Print the version number and exit.
.El
.Sh CONFIGURATION
This utility can be configured by adding entries to Git configuration.
.Pp
The following configuration keys are supported:
.Bl -tag
.It gitreview.username
Default username used to access the repository. If not specified
in the Git configuration, Git remote or
.Pa .gitreview
file, the user will be prompted to specify the username.
.Pp
Example entry in the
.Pa .gitconfig
file:
.Bd -literal -offset indent
[gitreview]
username=\fImygerrituser\fP
.Ed
.It gitreview.scheme
This setting determines the default scheme (ssh/http/https) of gerrit remote
.It gitreview.host
This setting determines the default hostname of gerrit remote
.It gitreview.port
This setting determines the default port of gerrit remote
.It gitreview.project
This setting determines the default name of gerrit git repo
.It gitreview.remote
This setting determines the default name to use for gerrit remote
.It gitreview.branch
This setting determines the default branch
.It gitreview.track
Determines whether to prefer the currently-tracked branch (if any)
and the branch against which the changeset was submitted to Gerrit
(if there is exactly one such branch) to the defaultremote and
defaultbranch for submitting and rebasing against.
If the local topic branch is tracking a remote branch, the remote
and branch that the local topic branch is tracking should be used
for submit and rebase operations, rather than the defaultremote
and defaultbranch.
.Pp
When downloading a patch, creates the local branch to track the
appropriate remote and branch in order to choose that branch by
default when submitting modifications to that changeset.
.Pp
A value of 'true' or 'false' should be specified.
.Bl -tag
.It true
Do prefer the currently-tracked branch (if any) \- equivalent
to setting
.Fl \-track
when submitting changes.
.It false
Ignore tracking branches \- equivalent to setting
.Fl \-no\-track
(the default) or providing an explicit branch name when submitting
changes. This is the default value unless overridden by
.Pa .gitreview
file, and is implied by providing a specific branch name on the
command line.
.El
.It gitreview.rebase
This setting determines whether changes submitted will
be rebased to the newest state of the branch.
.Pp
A value of 'true' or 'false' should be specified.
.Bl -tag
.It false
Do not rebase changes on submit \- equivalent to setting
.Fl R
when submitting changes.
.It true
Do rebase changes on submit. This is the default value unless
overridden by
.Pa .gitreview
file.
.El
.Pp
This setting takes precedence over repository\-specific configuration
in the
.Pa .gitreview
file.
.El
.Bl -tag
.It color.review
Whether to use ANSI escape sequences to add color to the output displayed by
this command. Default value is determined by color.ui.
.Bl -tag
.It auto or true
If you want output to use color when written to the terminal (default with Git
1.8.4 and newer).
.It always
If you want all output to use color
.It never or false
If you wish not to use color for any output. (default with Git older than 1.8.4)
.El
.El
.Pp
.Nm
will query git credential system for gerrit user/password when
authentication failed over http(s). Unlike git,
.Nm
does not persist gerrit user/password in git credential system for security
purposes and git credential system configuration stays under user responsibility.
.Sh FILES
To use
.Nm
with your project, it is recommended that you create
a file at the root of the repository named
.Pa .gitreview
and place information about your gerrit installation in it. The format is similar to the Windows .ini file format:
.Bd -literal -offset indent
[gerrit]
host=\fIhostname\fP
port=\fITCP port number of gerrit\fP
project=\fIproject name\fP
defaultbranch=\fIbranch to work on\fP
.Ed
.Pp
It is also possible to specify optional default name for
the Git remote using the
.Cm defaultremote
configuration parameter.
.Pp
Setting
.Cm defaultrebase
to zero will make
.Nm
not to rebase changes by default (same as the
.Fl R
command line option)
.Bd -literal -offset indent
[gerrit]
scheme=ssh
host=review.example.com
port=29418
project=department/project.git
defaultbranch=master
defaultremote=review
defaultrebase=0
track=0
.Ed
.Pp
When the same option is provided through FILES and CONFIGURATION, the
CONFIGURATION value wins.
.Pp
.Sh DIAGNOSTICS
.Pp
Normally, exit status is 0 if executed successfully.
Exit status 1 indicates general error, sometimes more
specific error codes are available:
.Bl -tag -width 999
.It 2
Gerrit
.Ar commit\-msg
hook could not be successfully installed.
.It 3
Could not parse malformed argument value or user input.
.It 32
Cannot fetch list of open changesets from Gerrit.
.It 33
Cannot parse list of open changesets received from Gerrit.
.It 34
Cannot query information about changesets.
.It 35
Cannot fetch information about the changeset to be downloaded.
.It 36
Changeset not found.
.It 37
Particular patchset cannot be fetched from the remote git repository.
.It 38
Specified patchset number not found in the changeset.
.It 39
Invalid patchsets for comparison.
.It 64
Cannot checkout downloaded patchset into the new branch.
.It 65
Cannot checkout downloaded patchset into existing branch.
.It 66
Cannot hard reset working directory and git index after download.
.It 67
Cannot switch to some other branch when trying to finish
the current branch.
.It 68
Cannot delete current branch.
.It 69
Requested patchset cannot be fully applied to the current branch. This exit
status will be returned when there are merge conflicts with the current branch.
Possible reasons include an attempt to apply patchset from the different branch
or code. This exit status will also be returned if the patchset is already
applied to the current branch.
.It 70
Cannot determine top level Git directory or .git subdirectory path.
.It 101
Unauthorized (401) http request done by git-review.
.It 104
Not Found (404) http request done by git-review.
.El
.Pp
Exit status larger than 31 indicates problem with
communication with Gerrit or remote Git repository,
exit status larger than 63 means there was a problem with
a local repository or a working copy.
.Pp
Exit status larger than or equal to 128 means internal
error in running the "git" command.
.Pp
.Sh EXAMPLES
To fetch a remote change number 3004:
.Pp
.Bd -literal -offset indent
$ git\-review \-d 3004
Downloading refs/changes/04/3004/1 from gerrit into
review/someone/topic_name
Switched to branch 'review/someone/topic_name
$ git branch
master
* review/author/topic_name
.Ed
.Pp
Gerrit looks up both name of the author and the topic name from Gerrit
to name a local branch. This facilitates easier identification of changes.
.Pp
To fetch a remote patchset number 5 from change number 3004:
.Pp
.Bd -literal -offset indent
$ git\-review \-d 3004,5
Downloading refs/changes/04/3004/5 from gerrit into
review/someone/topic_name\-patch5
Switched to branch 'review/someone/topic_name\-patch5
$ git branch
master
* review/author/topic_name\-patch5
.Ed
.Pp
To send a change for review and delete local branch afterwards:
.Bd -literal -offset indent
$ git\-review \-f
remote: Resolving deltas: 0% (0/8)
To ssh://username@review.example.com/department/project.git
* [new branch] HEAD \-> refs/for/master/topic_name
Switched to branch 'master'
Deleted branch 'review/someone/topic_name'
$ git branch
* master
.Ed
.Pp
An example
.Pa .gitreview
configuration file for a project
.Pa department/project
hosted on
.Cm review.example.com
port
.Cm 29418
in the branch
.Cm master
:
.Bd -literal -offset indent
[gerrit]
host=review.example.com
port=29418
project=department/project.git
defaultbranch=master
.Ed
.Sh BUGS
Bug reports can be submitted to
.Lk https://launchpad.net/git\-review
.Sh AUTHORS
.Nm
is maintained by
.An "OpenStack, LLC"
.Pp
This manpage has been enhanced by:
.An "Antoine Musso" Aq hashar@free.fr
.An "Marcin Cieslak" Aq saper@saper.info
.An "Pavel Sedlák" Aq psedlak@redhat.com

277
git_restack/cmd.py Executable file
View File

@ -0,0 +1,277 @@
#!/usr/bin/env python
from __future__ import print_function
COPYRIGHT = """\
Copyright (C) 2011-2012 OpenStack LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied.
See the License for the specific language governing permissions and
limitations under the License."""
import argparse
import datetime
import os
import shlex
import subprocess
import sys
import pkg_resources
if sys.version < '3':
import ConfigParser
import urllib
import urlparse
urlencode = urllib.urlencode
urljoin = urlparse.urljoin
urlparse = urlparse.urlparse
do_input = raw_input
else:
import configparser as ConfigParser
import urllib.parse
import urllib.request
urlencode = urllib.parse.urlencode
urljoin = urllib.parse.urljoin
urlparse = urllib.parse.urlparse
do_input = input
VERBOSE = False
UPDATE = False
LOCAL_MODE = 'GITREVIEW_LOCAL_MODE' in os.environ
CONFIGDIR = os.path.expanduser("~/.config/git-review")
GLOBAL_CONFIG = "/etc/git-review/git-review.conf"
USER_CONFIG = os.path.join(CONFIGDIR, "git-review.conf")
DEFAULTS = dict(branch='master')
class GitRestackException(Exception):
pass
class CommandFailed(GitRestackException):
def __init__(self, *args):
Exception.__init__(self, *args)
(self.rc, self.output, self.argv, self.envp) = args
self.quickmsg = dict([
("argv", " ".join(self.argv)),
("rc", self.rc),
("output", self.output)])
def __str__(self):
return self.__doc__ + """
The following command failed with exit code %(rc)d
"%(argv)s"
-----------------------
%(output)s
-----------------------""" % self.quickmsg
class GitDirectoriesException(CommandFailed):
"Cannot determine where .git directory is."
EXIT_CODE = 70
class GitMergeBaseException(CommandFailed):
"Cannot determine merge base."
EXIT_CODE = 71
class GitConfigException(CommandFailed):
"""Git config value retrieval failed."""
EXIT_CODE = 128
def run_command_foreground(*argv, **kwargs):
if VERBOSE:
print(datetime.datetime.now(), "Running:", " ".join(argv))
if len(argv) == 1:
# for python2 compatibility with shlex
if sys.version_info < (3,) and isinstance(argv[0], unicode):
argv = shlex.split(argv[0].encode('utf-8'))
else:
argv = shlex.split(str(argv[0]))
subprocess.call(argv)
def run_command_status(*argv, **kwargs):
if VERBOSE:
print(datetime.datetime.now(), "Running:", " ".join(argv))
if len(argv) == 1:
# for python2 compatibility with shlex
if sys.version_info < (3,) and isinstance(argv[0], unicode):
argv = shlex.split(argv[0].encode('utf-8'))
else:
argv = shlex.split(str(argv[0]))
stdin = kwargs.pop('stdin', None)
newenv = os.environ.copy()
newenv['LANG'] = 'C'
newenv['LANGUAGE'] = 'C'
newenv.update(kwargs)
p = subprocess.Popen(argv,
stdin=subprocess.PIPE if stdin else None,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=newenv)
(out, nothing) = p.communicate(stdin)
out = out.decode('utf-8', 'replace')
return (p.returncode, out.strip())
def run_command(*argv, **kwargs):
(rc, output) = run_command_status(*argv, **kwargs)
return output
def run_command_exc(klazz, *argv, **env):
"""Run command *argv, on failure raise klazz
klazz should be derived from CommandFailed
"""
(rc, output) = run_command_status(*argv, **env)
if rc != 0:
raise klazz(rc, output, argv, env)
return output
def get_version():
requirement = pkg_resources.Requirement.parse('git-restack')
provider = pkg_resources.get_provider(requirement)
return provider.version
def git_directories():
"""Determine (absolute git work directory path, .git subdirectory path)."""
cmd = ("git", "rev-parse", "--show-toplevel", "--git-dir")
out = run_command_exc(GitDirectoriesException, *cmd)
try:
return out.splitlines()
except ValueError:
raise GitDirectoriesException(0, out, cmd, {})
def git_config_get_value(section, option, default=None, as_bool=False):
"""Get config value for section/option."""
cmd = ["git", "config", "--get", "%s.%s" % (section, option)]
if as_bool:
cmd.insert(2, "--bool")
if LOCAL_MODE:
__, git_dir = git_directories()
cmd[2:2] = ['-f', os.path.join(git_dir, 'config')]
try:
return run_command_exc(GitConfigException, *cmd).strip()
except GitConfigException as exc:
if exc.rc == 1:
return default
raise
class Config(object):
"""Expose as dictionary configuration options."""
def __init__(self, config_file=None):
self.config = DEFAULTS.copy()
filenames = [] if LOCAL_MODE else [GLOBAL_CONFIG, USER_CONFIG]
if config_file:
filenames.append(config_file)
for filename in filenames:
if os.path.exists(filename):
if filename != config_file:
msg = ("Using global/system git-review config files (%s) "
"is deprecated")
print(msg % filename)
self.config.update(load_config_file(filename))
def __getitem__(self, key):
value = git_config_get_value('gitreview', key)
if value is None:
value = self.config[key]
return value
def load_config_file(config_file):
"""Load configuration options from a file."""
configParser = ConfigParser.ConfigParser()
configParser.read(config_file)
options = {
'scheme': 'scheme',
'hostname': 'host',
'port': 'port',
'project': 'project',
'branch': 'defaultbranch',
'remote': 'defaultremote',
'rebase': 'defaultrebase',
'track': 'track',
'usepushurl': 'usepushurl',
}
config = {}
for config_key, option_name in options.items():
if configParser.has_option('gerrit', option_name):
config[config_key] = configParser.get('gerrit', option_name)
return config
def main():
usage = "git restack [BRANCH]"
parser = argparse.ArgumentParser(usage=usage, description=COPYRIGHT)
parser.add_argument("-v", "--verbose", dest="verbose", action="store_true",
help="Output more information about what's going on")
parser.add_argument("--license", dest="license", action="store_true",
help="Print the license and exit")
parser.add_argument("--version", action="version",
version='%s version %s' %
(os.path.split(sys.argv[0])[-1], get_version()))
parser.add_argument("branch", nargs="?")
parser.set_defaults(verbose=False)
try:
(top_dir, git_dir) = git_directories()
except GitDirectoriesException as no_git_dir:
pass
else:
no_git_dir = False
config = Config(os.path.join(top_dir, ".gitreview"))
options = parser.parse_args()
if no_git_dir:
raise no_git_dir
if options.license:
print(COPYRIGHT)
sys.exit(0)
global VERBOSE
VERBOSE = options.verbose
if options.branch is None:
branch = config['branch']
else:
branch = options.branch
if branch is None:
branch = 'master'
status = 0
cmd = "git merge-base HEAD origin/%s" % branch
base = run_command_exc(GitMergeBaseException, cmd)
run_command_foreground("git rebase -i %s" % base, stdin=sys.stdin)
sys.exit(status)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,86 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import fixtures
import testtools
from git_restack.tests import utils
class BaseGitRestackTestCase(testtools.TestCase):
"""Base class for the git-restack tests."""
def setUp(self):
"""Configure testing environment.
Prepare directory for the testing and clone test Git repository.
Require Gerrit war file in the .gerrit directory to run Gerrit local.
"""
super(BaseGitRestackTestCase, self).setUp()
self.useFixture(fixtures.Timeout(2 * 60, True))
self.root_dir = self.useFixture(fixtures.TempDir()).path
self.upstream_dir = os.path.join(self.root_dir, "upstream")
self.local_dir = os.path.join(self.root_dir, "local")
os.makedirs(self._dir('upstream'))
self._run_git('upstream', 'init')
self._simple_change('upstream', 'initial text', 'initial commit')
self._simple_change('upstream', 'second text', 'second commit')
self._run_git('upstream', 'checkout', '-b', 'branch1')
self._simple_change('upstream', 'branch1 text', 'branch1 commit')
self._run_git('upstream', 'checkout', 'master')
self._run_git('upstream', 'checkout', '-b', 'branch2')
gitreview = '[gerrit]\ndefaultbranch=branch2\n'
self._simple_change('upstream', gitreview, 'branch2 commit',
file_=self._dir('upstream', '.gitreview'))
self._run_git('upstream', 'checkout', 'master')
def _dir(self, base, *args):
"""Creates directory name from base name and other parameters."""
return os.path.join(getattr(self, base + '_dir'), *args)
def _run_git(self, dirname, command, *args):
"""Run git command using test git directory."""
if command == 'clone':
return utils.run_git(command, args[0], self._dir(dirname))
return utils.run_git('--git-dir=' + self._dir(dirname, '.git'),
'--work-tree=' + self._dir(dirname),
command, *args)
def _run_git_restack(self, *args, **kwargs):
"""Run git-restack utility from source."""
git_restack = utils.run_cmd('which', 'git-restack')
kwargs.setdefault('chdir', self.local_dir)
return utils.run_cmd(git_restack, *args, **kwargs)
def _simple_change(self, dirname, change_text, commit_message,
file_=None):
"""Helper method to create small changes and commit them."""
if file_ is None:
file_ = self._dir(dirname, 'test_file.txt')
utils.write_to_file(file_, change_text.encode())
self._run_git(dirname, 'add', file_)
self._run_git(dirname, 'commit', '-m', commit_message)
def _git_log(self, dirname):
out = self._run_git(dirname, 'log', '--oneline')
commits = []
for line in out.split('\n'):
commits.append(line.split(' ', 1))
return commits

View File

@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from git_restack import tests
class GitRestackTestCase(tests.BaseGitRestackTestCase):
"""Class for config tests."""
def test_git_restack(self):
self._run_git('local', 'clone', self._dir('upstream'))
self._simple_change('local', 'b1 text', 'b1')
self._simple_change('local', 'b2 text', 'b2')
self._simple_change('local', 'b3 text', 'b3')
commits = self._git_log('local')
self.assertEqual(commits[0][1], 'b3')
self.assertEqual(commits[1][1], 'b2')
self.assertEqual(commits[2][1], 'b1')
self.assertEqual(commits[3][1], 'second commit')
self.assertEqual(commits[4][1], 'initial commit')
out = self._run_git_restack()
lines = out.split('\n')
self.assertEqual(lines[0], 'pick %s %s' %
(commits[2][0], commits[2][1]))
self.assertEqual(lines[1], 'pick %s %s' %
(commits[1][0], commits[1][1]))
self.assertEqual(lines[2], 'pick %s %s' %
(commits[0][0], commits[0][1]))
self.assertEqual(lines[3], '')
def test_git_restack_gitreview(self):
self._run_git('local', 'clone', self._dir('upstream'))
self._run_git('local', 'checkout', 'branch2')
self._simple_change('local', 'b1 text', 'b1')
self._simple_change('local', 'b2 text', 'b2')
self._simple_change('local', 'b3 text', 'b3')
commits = self._git_log('local')
self.assertEqual(commits[0][1], 'b3')
self.assertEqual(commits[1][1], 'b2')
self.assertEqual(commits[2][1], 'b1')
self.assertEqual(commits[3][1], 'branch2 commit')
self.assertEqual(commits[4][1], 'second commit')
self.assertEqual(commits[5][1], 'initial commit')
out = self._run_git_restack()
lines = out.split('\n')
self.assertEqual(lines[0], 'pick %s %s' %
(commits[2][0], commits[2][1]))
self.assertEqual(lines[1], 'pick %s %s' %
(commits[1][0], commits[1][1]))
self.assertEqual(lines[2], 'pick %s %s' %
(commits[0][0], commits[0][1]))
self.assertEqual(lines[3], '')
def test_git_restack_arg(self):
self._run_git('local', 'clone', self._dir('upstream'))
self._run_git('local', 'checkout', 'branch1')
self._simple_change('local', 'b1 text', 'b1')
self._simple_change('local', 'b2 text', 'b2')
self._simple_change('local', 'b3 text', 'b3')
commits = self._git_log('local')
self.assertEqual(commits[0][1], 'b3')
self.assertEqual(commits[1][1], 'b2')
self.assertEqual(commits[2][1], 'b1')
self.assertEqual(commits[3][1], 'branch1 commit')
self.assertEqual(commits[4][1], 'second commit')
self.assertEqual(commits[5][1], 'initial commit')
out = self._run_git_restack('branch1')
lines = out.split('\n')
self.assertEqual(lines[0], 'pick %s %s' %
(commits[2][0], commits[2][1]))
self.assertEqual(lines[1], 'pick %s %s' %
(commits[1][0], commits[1][1]))
self.assertEqual(lines[2], 'pick %s %s' %
(commits[0][0], commits[0][1]))
self.assertEqual(lines[3], '')

View File

@ -27,6 +27,7 @@ def run_cmd(*args, **kwargs):
return os.chdir(kwargs['chdir']) return os.chdir(kwargs['chdir'])
try: try:
os.environ['EDITOR'] = '/bin/cat'
proc = subprocess.Popen(args, stdin=subprocess.PIPE, proc = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, env=os.environ, stderr=subprocess.STDOUT, env=os.environ,

File diff suppressed because it is too large Load Diff

View File

@ -1,335 +0,0 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import shutil
import stat
import sys
if sys.version < '3':
import urllib
import urlparse
urlparse = urlparse.urlparse
else:
import urllib.parse
import urllib.request
urlparse = urllib.parse.urlparse
import fixtures
import requests
import testtools
from testtools import content
from git_review.tests import utils
WAR_URL = 'https://gerrit-releases.storage.googleapis.com/gerrit-2.9.2.war'
# Update GOLDEN_SITE_VER for every change altering golden site, including
# WAR_URL changes. Set new value to something unique (just +1 it for example)
GOLDEN_SITE_VER = '2'
class GerritHelpers(object):
def _dir(self, base, *args):
"""Creates directory name from base name and other parameters."""
return os.path.join(getattr(self, base + '_dir'), *args)
def init_dirs(self):
self.primary_dir = os.path.abspath(os.path.curdir)
self.gerrit_dir = self._dir('primary', '.gerrit')
self.gsite_dir = self._dir('gerrit', 'golden_site')
self.gerrit_war = self._dir('gerrit', WAR_URL.split('/')[-1])
def ensure_gerrit_war(self):
# check if gerrit.war file exists in .gerrit directory
if not os.path.exists(self.gerrit_dir):
os.mkdir(self.gerrit_dir)
if not os.path.exists(self.gerrit_war):
print("Downloading Gerrit binary from %s..." % WAR_URL)
resp = requests.get(WAR_URL)
if resp.status_code != 200:
raise RuntimeError("Problem requesting Gerrit war")
utils.write_to_file(self.gerrit_war, resp.content)
print("Saved to %s" % self.gerrit_war)
def init_gerrit(self):
"""Run Gerrit from the war file and configure it."""
golden_ver_file = self._dir('gsite', 'golden_ver')
if os.path.exists(self.gsite_dir):
if not os.path.exists(golden_ver_file):
golden_ver = '0'
else:
with open(golden_ver_file) as f:
golden_ver = f.read().strip()
if GOLDEN_SITE_VER != golden_ver:
print("Existing golden site has version %s, removing..." %
golden_ver)
shutil.rmtree(self.gsite_dir)
else:
print("Golden site of version %s already exists" %
GOLDEN_SITE_VER)
return
print("Creating a new golden site of version " + GOLDEN_SITE_VER)
# initialize Gerrit
utils.run_cmd('java', '-jar', self.gerrit_war,
'init', '-d', self.gsite_dir,
'--batch', '--no-auto-start', '--install-plugin',
'download-commands')
with open(golden_ver_file, 'w') as f:
f.write(GOLDEN_SITE_VER)
# create SSH public key
key_file = self._dir('gsite', 'test_ssh_key')
utils.run_cmd('ssh-keygen', '-t', 'rsa', '-b', '4096',
'-f', key_file, '-N', '')
with open(key_file + '.pub', 'rb') as pub_key_file:
pub_key = pub_key_file.read()
# create admin user in Gerrit database
sql_query = """INSERT INTO ACCOUNTS (REGISTERED_ON) VALUES (NOW());
INSERT INTO ACCOUNT_GROUP_MEMBERS (ACCOUNT_ID, GROUP_ID) \
VALUES (0, 1);
INSERT INTO ACCOUNT_EXTERNAL_IDS (ACCOUNT_ID, EXTERNAL_ID, PASSWORD) \
VALUES (0, 'username:test_user', 'test_pass');
INSERT INTO ACCOUNT_SSH_KEYS (SSH_PUBLIC_KEY, VALID) \
VALUES ('%s', 'Y')""" % pub_key.decode()
utils.run_cmd('java', '-jar',
self._dir('gsite', 'bin', 'gerrit.war'),
'gsql', '-d', self.gsite_dir, '-c', sql_query)
def _run_gerrit_cli(self, command, *args):
"""SSH to gerrit Gerrit server and run command there."""
return utils.run_cmd('ssh', '-p', str(self.gerrit_port),
'test_user@' + self.gerrit_host, 'gerrit',
command, *args)
def _run_git_review(self, *args, **kwargs):
"""Run git-review utility from source."""
git_review = utils.run_cmd('which', 'git-review')
kwargs.setdefault('chdir', self.test_dir)
return utils.run_cmd(git_review, *args, **kwargs)
class BaseGitReviewTestCase(testtools.TestCase, GerritHelpers):
"""Base class for the git-review tests."""
_test_counter = 0
_remote = 'gerrit'
@property
def project_uri(self):
return self.project_ssh_uri
def setUp(self):
"""Configure testing environment.
Prepare directory for the testing and clone test Git repository.
Require Gerrit war file in the .gerrit directory to run Gerrit local.
"""
super(BaseGitReviewTestCase, self).setUp()
self.useFixture(fixtures.Timeout(2 * 60, True))
BaseGitReviewTestCase._test_counter += 1
# ensures git-review command runs in local mode (for functional tests)
self.useFixture(
fixtures.EnvironmentVariable('GITREVIEW_LOCAL_MODE', ''))
self.init_dirs()
ssh_addr, ssh_port, http_addr, http_port, self.site_dir = \
self._pick_gerrit_port_and_dir()
self.gerrit_host, self.gerrit_port = ssh_addr, ssh_port
self.test_dir = self._dir('site', 'tmp', 'test_project')
self.ssh_dir = self._dir('site', 'tmp', 'ssh')
self.project_ssh_uri = (
'ssh://test_user@%s:%s/test/test_project.git' % (
ssh_addr, ssh_port))
self.project_http_uri = (
'http://test_user:test_pass@%s:%s/test/test_project.git' % (
http_addr, http_port))
self._run_gerrit(ssh_addr, ssh_port, http_addr, http_port)
self._configure_ssh(ssh_addr, ssh_port)
# create Gerrit empty project
self._run_gerrit_cli('create-project', '--empty-commit',
'--name', 'test/test_project')
# ensure user proxy conf doesn't interfere with tests
os.environ['no_proxy'] = os.environ['NO_PROXY'] = '*'
# isolate tests from user and system git configuration
self.home_dir = self._dir('site', 'tmp', 'home')
self.xdg_config_dir = self._dir('home', '.xdgconfig')
os.environ['HOME'] = self.home_dir
os.environ['XDG_CONFIG_HOME'] = self.xdg_config_dir
os.environ['GIT_CONFIG_NOSYSTEM'] = "1"
os.environ['EMAIL'] = "you@example.com"
if not os.path.exists(self.home_dir):
os.mkdir(self.home_dir)
if not os.path.exists(self.xdg_config_dir):
os.mkdir(self.xdg_config_dir)
self.addCleanup(shutil.rmtree, self.home_dir)
# prepare repository for the testing
self._run_git('clone', self.project_uri)
utils.write_to_file(self._dir('test', 'test_file.txt'),
'test file created'.encode())
self._create_gitreview_file()
# push changes to the Gerrit
self._run_git('add', '--all')
self._run_git('commit', '-m', 'Test file and .gitreview added.')
self._run_git('push', 'origin', 'master')
shutil.rmtree(self.test_dir)
# go to the just cloned test Git repository
self._run_git('clone', self.project_uri)
self.configure_gerrit_remote()
self.addCleanup(shutil.rmtree, self.test_dir)
# ensure user is configured for all tests
self._configure_gitreview_username()
def set_remote(self, uri):
self._run_git('remote', 'set-url', self._remote, uri)
def reset_remote(self):
self._run_git('remote', 'rm', self._remote)
def attach_on_exception(self, filename):
@self.addOnException
def attach_file(exc_info):
if os.path.exists(filename):
content.attach_file(self, filename)
else:
self.addDetail(os.path.basename(filename),
content.text_content('Not found'))
def _run_git(self, command, *args):
"""Run git command using test git directory."""
if command == 'clone':
return utils.run_git(command, args[0], self._dir('test'))
return utils.run_git('--git-dir=' + self._dir('test', '.git'),
'--work-tree=' + self._dir('test'),
command, *args)
def _run_gerrit(self, ssh_addr, ssh_port, http_addr, http_port):
# create a copy of site dir
shutil.copytree(self.gsite_dir, self.site_dir)
self.addCleanup(shutil.rmtree, self.site_dir)
# write config
with open(self._dir('site', 'etc', 'gerrit.config'), 'w') as _conf:
new_conf = utils.get_gerrit_conf(
ssh_addr, ssh_port, http_addr, http_port)
_conf.write(new_conf)
# If test fails, attach Gerrit config and logs to the result
self.attach_on_exception(self._dir('site', 'etc', 'gerrit.config'))
for name in ['error_log', 'sshd_log', 'httpd_log']:
self.attach_on_exception(self._dir('site', 'logs', name))
# start Gerrit
gerrit_sh = self._dir('site', 'bin', 'gerrit.sh')
utils.run_cmd(gerrit_sh, 'start')
self.addCleanup(utils.run_cmd, gerrit_sh, 'stop')
def _simple_change(self, change_text, commit_message,
file_=None):
"""Helper method to create small changes and commit them."""
if file_ is None:
file_ = self._dir('test', 'test_file.txt')
utils.write_to_file(file_, change_text.encode())
self._run_git('add', file_)
self._run_git('commit', '-m', commit_message)
def _simple_amend(self, change_text, file_=None):
"""Helper method to amend existing commit with change."""
if file_ is None:
file_ = self._dir('test', 'test_file_new.txt')
utils.write_to_file(file_, change_text.encode())
self._run_git('add', file_)
# cannot use --no-edit because it does not exist in older git
message = self._run_git('log', '-1', '--format=%s\n\n%b')
self._run_git('commit', '--amend', '-m', message)
def _configure_ssh(self, ssh_addr, ssh_port):
"""Setup ssh and scp to run with special options."""
os.mkdir(self.ssh_dir)
ssh_key = utils.run_cmd('ssh-keyscan', '-p', str(ssh_port), ssh_addr)
utils.write_to_file(self._dir('ssh', 'known_hosts'), ssh_key.encode())
self.addCleanup(os.remove, self._dir('ssh', 'known_hosts'))
# Attach known_hosts to test results if anything fails
self.attach_on_exception(self._dir('ssh', 'known_hosts'))
for cmd in ('ssh', 'scp'):
cmd_file = self._dir('ssh', cmd)
s = '#!/bin/sh\n' \
'/usr/bin/%s -i %s -o UserKnownHostsFile=%s ' \
'-o IdentitiesOnly=yes ' \
'-o PasswordAuthentication=no $@' % \
(cmd,
self._dir('gsite', 'test_ssh_key'),
self._dir('ssh', 'known_hosts'))
utils.write_to_file(cmd_file, s.encode())
os.chmod(cmd_file, os.stat(cmd_file).st_mode | stat.S_IEXEC)
os.environ['PATH'] = self.ssh_dir + os.pathsep + os.environ['PATH']
os.environ['GIT_SSH'] = self._dir('ssh', 'ssh')
def configure_gerrit_remote(self):
self._run_git('remote', 'add', self._remote, self.project_uri)
def _configure_gitreview_username(self):
self._run_git('config', 'gitreview.username', 'test_user')
def _pick_gerrit_port_and_dir(self):
pid = os.getpid()
host = '127.%s.%s.%s' % (self._test_counter, pid >> 8, pid & 255)
return host, 29418, host, 8080, self._dir('gerrit', 'site-' + host)
def _create_gitreview_file(self, **kwargs):
cfg = ('[gerrit]\n'
'scheme=%s\n'
'host=%s\n'
'port=%s\n'
'project=test/test_project.git\n'
'%s')
parsed = urlparse(self.project_uri)
host_port = parsed.netloc.rpartition('@')[-1]
host, __, port = host_port.partition(':')
extra = '\n'.join('%s=%s' % kv for kv in kwargs.items())
cfg %= parsed.scheme, host, port, extra
utils.write_to_file(self._dir('test', '.gitreview'), cfg.encode())
class HttpMixin(object):
"""HTTP remote_url mixin."""
@property
def project_uri(self):
return self.project_http_uri
def _configure_gitreview_username(self):
# trick to set http password
self._run_git('config', 'gitreview.username', 'test_user:test_pass')

View File

@ -1,26 +0,0 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from git_review import tests
def main():
helpers = tests.GerritHelpers()
helpers.init_dirs()
helpers.ensure_gerrit_war()
helpers.init_gerrit()
if __name__ == "__main__":
main()

View File

@ -1,554 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import shutil
from git_review import tests
from git_review.tests import utils
class ConfigTestCase(tests.BaseGitReviewTestCase):
"""Class for config tests."""
def test_get_config_from_cli(self):
self.reset_remote()
self._run_git('remote', 'rm', 'origin')
self._create_gitreview_file(defaultremote='remote-file')
self._run_git('config', 'gitreview.remote', 'remote-gitconfig')
self._run_git_review('-s', '-r', 'remote-cli')
remote = self._run_git('remote').strip()
self.assertEqual('remote-cli', remote)
def test_get_config_from_gitconfig(self):
self.reset_remote()
self._run_git('remote', 'rm', 'origin')
self._create_gitreview_file(defaultremote='remote-file')
self._run_git('config', 'gitreview.remote', 'remote-gitconfig')
self._run_git_review('-s')
remote = self._run_git('remote').strip()
self.assertEqual('remote-gitconfig', remote)
def test_get_config_from_file(self):
self.reset_remote()
self._run_git('remote', 'rm', 'origin')
self._create_gitreview_file(defaultremote='remote-file')
self._run_git_review('-s')
remote = self._run_git('remote').strip()
self.assertEqual('remote-file', remote)
class GitReviewTestCase(tests.BaseGitReviewTestCase):
"""Class for the git-review tests."""
def test_cloned_repo(self):
"""Test git-review on the just cloned repository."""
self._simple_change('test file modified', 'test commit message')
self.assertNotIn('Change-Id:', self._run_git('log', '-1'))
self.assertIn('remote: New Changes:', self._run_git_review())
self.assertIn('Change-Id:', self._run_git('log', '-1'))
def test_git_review_s(self):
"""Test git-review -s."""
self.reset_remote()
self._run_git_review('-s')
self._simple_change('test file modified', 'test commit message')
self.assertIn('Change-Id:', self._run_git('log', '-1'))
def test_git_review_s_in_detached_head(self):
"""Test git-review -s in detached HEAD state."""
self.reset_remote()
master_sha1 = self._run_git('rev-parse', 'master')
self._run_git('checkout', master_sha1)
self._run_git_review('-s')
self._simple_change('test file modified', 'test commit message')
self.assertIn('Change-Id:', self._run_git('log', '-1'))
def test_git_review_s_with_outdated_repo(self):
"""Test git-review -s with a outdated repo."""
self._simple_change('test file to outdate', 'test commit message 1')
self._run_git('push', 'origin', 'master')
self._run_git('reset', '--hard', 'HEAD^')
# Review setup with an outdated repo
self.reset_remote()
self._run_git_review('-s')
self._simple_change('test file modified', 'test commit message 2')
self.assertIn('Change-Id:', self._run_git('log', '-1'))
def test_git_review_s_from_subdirectory(self):
"""Test git-review -s from subdirectory."""
self.reset_remote()
utils.run_cmd('mkdir', 'subdirectory', chdir=self.test_dir)
self._run_git_review(
'-s', chdir=os.path.join(self.test_dir, 'subdirectory'))
def test_git_review_d(self):
"""Test git-review -d."""
self._run_git_review('-s')
# create new review to be downloaded
self._simple_change('test file modified', 'test commit message')
self._run_git_review()
change_id = self._run_git('log', '-1').split()[-1]
shutil.rmtree(self.test_dir)
# download clean Git repository and fresh change from Gerrit to it
self._run_git('clone', self.project_uri)
self.configure_gerrit_remote()
self._run_git_review('-d', change_id)
self.assertIn('test commit message', self._run_git('log', '-1'))
# second download should also work correct
self._run_git_review('-d', change_id)
self.assertIn('test commit message', self._run_git('show', 'HEAD'))
self.assertNotIn('test commit message',
self._run_git('show', 'HEAD^1'))
# and branch is tracking
head = self._run_git('symbolic-ref', '-q', 'HEAD')
self.assertIn(
'refs/remotes/%s/master' % self._remote,
self._run_git("for-each-ref", "--format='%(upstream)'", head))
def test_multiple_changes(self):
"""Test git-review asks about multiple changes.
Should register user's wish to send two change requests by interactive
'yes' message and by the -y option.
"""
self._run_git_review('-s')
# 'yes' message
self._simple_change('test file modified 1st time',
'test commit message 1')
self._simple_change('test file modified 2nd time',
'test commit message 2')
review_res = self._run_git_review(confirm=True)
self.assertIn("Type 'yes' to confirm", review_res)
self.assertIn("Processing changes: new: 2", review_res)
# abandon changes sent to the Gerrit
head = self._run_git('rev-parse', 'HEAD')
head_1 = self._run_git('rev-parse', 'HEAD^1')
self._run_gerrit_cli('review', '--abandon', head)
self._run_gerrit_cli('review', '--abandon', head_1)
# -y option
self._simple_change('test file modified 3rd time',
'test commit message 3')
self._simple_change('test file modified 4th time',
'test commit message 4')
review_res = self._run_git_review('-y')
self.assertIn("Processing changes: new: 2", review_res)
def test_git_review_re(self):
"""Test git-review adding reviewers to changes."""
self._run_git_review('-s')
# Create users to add as reviewers
self._run_gerrit_cli('create-account', '--email',
'reviewer1@example.com', 'reviewer1')
self._run_gerrit_cli('create-account', '--email',
'reviewer2@example.com', 'reviewer2')
self._simple_change('test file', 'test commit message')
review_res = self._run_git_review('--reviewers', 'reviewer1',
'reviewer2')
self.assertIn("Processing changes: new: 1", review_res)
# verify both reviewers are on patch set
head = self._run_git('rev-parse', 'HEAD')
change = self._run_gerrit_cli('query', '--format=JSON',
'--all-reviewers', head)
# The first result should be the one we want
change = json.loads(change.split('\n')[0])
self.assertEqual(2, len(change['allReviewers']))
reviewers = set()
for reviewer in change['allReviewers']:
reviewers.add(reviewer['username'])
self.assertEqual(set(['reviewer1', 'reviewer2']), reviewers)
def test_rebase_no_remote_branch_msg(self):
"""Test message displayed where no remote branch exists."""
self._run_git_review('-s')
self._run_git('checkout', '-b', 'new_branch')
self._simple_change('simple message',
'need to avoid noop message')
exc = self.assertRaises(Exception, self._run_git_review, 'new_branch')
self.assertIn("The branch 'new_branch' does not exist on the given "
"remote '%s'" % self._remote, exc.args[0])
def test_need_rebase_no_upload(self):
"""Test change needing a rebase does not upload."""
self._run_git_review('-s')
head_1 = self._run_git('rev-parse', 'HEAD^1')
self._run_git('checkout', '-b', 'test_branch', head_1)
self._simple_change('some other message',
'create conflict with master')
exc = self.assertRaises(Exception, self._run_git_review)
self.assertIn(
"Errors running git rebase -p -i remotes/%s/master" % self._remote,
exc.args[0])
self.assertIn("It is likely that your change has a merge conflict.",
exc.args[0])
def test_upload_without_rebase(self):
"""Test change not needing a rebase can upload without rebasing."""
self._run_git_review('-s')
head_1 = self._run_git('rev-parse', 'HEAD^1')
self._run_git('checkout', '-b', 'test_branch', head_1)
self._simple_change('some new message',
'just another file (no conflict)',
self._dir('test', 'new_test_file.txt'))
review_res = self._run_git_review('-v')
self.assertIn(
"Running: git rebase -p -i remotes/%s/master" % self._remote,
review_res)
self.assertEqual(self._run_git('rev-parse', 'HEAD^1'), head_1)
def test_uploads_with_nondefault_rebase(self):
"""Test changes rebase against correct branches."""
# prepare maintenance branch that is behind master
self._create_gitreview_file(track='true',
defaultremote='origin')
self._run_git('add', '.gitreview')
self._run_git('commit', '-m', 'track=true.')
self._simple_change('diverge master from maint',
'no conflict',
self._dir('test', 'test_file_to_diverge.txt'))
self._run_git('push', 'origin', 'master')
self._run_git('push', 'origin', 'master', 'master:other')
self._run_git_review('-s')
head_1 = self._run_git('rev-parse', 'HEAD^1')
self._run_gerrit_cli('create-branch',
'test/test_project',
'maint', head_1)
self._run_git('fetch')
br_out = self._run_git('checkout',
'-b', 'test_branch', 'origin/maint')
expected_track = 'Branch test_branch set up to track remote' + \
' branch maint from origin.'
self.assertIn(expected_track, br_out)
branches = self._run_git('branch', '-a')
expected_branch = '* test_branch'
observed = branches.split('\n')
self.assertIn(expected_branch, observed)
self._simple_change('some new message',
'just another file (no conflict)',
self._dir('test', 'new_tracked_test_file.txt'))
change_id = self._run_git('log', '-1').split()[-1]
review_res = self._run_git_review('-v')
# no rebase needed; if it breaks it would try to rebase to master
self.assertNotIn("Running: git rebase -p -i remotes/origin/master",
review_res)
# Don't need to query gerrit for the branch as the second half
# of this test will work only if the branch was correctly
# stored in gerrit
# delete branch locally
self._run_git('checkout', 'master')
self._run_git('branch', '-D', 'test_branch')
# download, amend, submit
self._run_git_review('-d', change_id)
self._simple_amend('just another file (no conflict)',
self._dir('test', 'new_tracked_test_file_2.txt'))
new_change_id = self._run_git('log', '-1').split()[-1]
self.assertEqual(change_id, new_change_id)
review_res = self._run_git_review('-v')
# caused the right thing to happen
self.assertIn("Running: git rebase -p -i remotes/origin/maint",
review_res)
# track different branch than expected in changeset
branch = self._run_git('rev-parse', '--abbrev-ref', 'HEAD')
self._run_git('branch',
'--set-upstream',
branch,
'remotes/origin/other')
self.assertRaises(
Exception, # cmd.BranchTrackingMismatch inside
self._run_git_review, '-d', change_id)
def test_no_rebase_check(self):
"""Test -R causes a change to be uploaded without rebase checking."""
self._run_git_review('-s')
head_1 = self._run_git('rev-parse', 'HEAD^1')
self._run_git('checkout', '-b', 'test_branch', head_1)
self._simple_change('some new message', 'just another file',
self._dir('test', 'new_test_file.txt'))
review_res = self._run_git_review('-v', '-R')
self.assertNotIn('rebase', review_res)
self.assertEqual(self._run_git('rev-parse', 'HEAD^1'), head_1)
def test_rebase_anyway(self):
"""Test -F causes a change to be rebased regardless."""
self._run_git_review('-s')
head = self._run_git('rev-parse', 'HEAD')
head_1 = self._run_git('rev-parse', 'HEAD^1')
self._run_git('checkout', '-b', 'test_branch', head_1)
self._simple_change('some new message', 'just another file',
self._dir('test', 'new_test_file.txt'))
review_res = self._run_git_review('-v', '-F')
self.assertIn('rebase', review_res)
self.assertEqual(self._run_git('rev-parse', 'HEAD^1'), head)
def _assert_branch_would_be(self, branch, extra_args=None):
extra_args = extra_args or []
output = self._run_git_review('-n', *extra_args)
# last non-empty line should be:
# git push gerrit HEAD:refs/publish/master
last_line = output.strip().split('\n')[-1]
branch_was = last_line.rsplit(' ', 1)[-1].split('/', 2)[-1]
self.assertEqual(branch, branch_was)
def test_detached_head(self):
"""Test on a detached state: we shouldn't have '(detached' as topic."""
self._run_git_review('-s')
curr_branch = self._run_git('rev-parse', '--abbrev-ref', 'HEAD')
# Note: git checkout --detach has been introduced in git 1.7.5 (2011)
self._run_git('checkout', curr_branch + '^0')
self._simple_change('some new message', 'just another file',
self._dir('test', 'new_test_file.txt'))
# switch to French, 'git branch' should return '(détaché du HEAD)'
lang_env = os.getenv('LANG', 'C')
os.environ.update(LANG='fr_FR.UTF-8')
try:
self._assert_branch_would_be(curr_branch)
finally:
os.environ.update(LANG=lang_env)
def test_git_review_t(self):
self._run_git_review('-s')
self._simple_change('test file modified', 'commit message for bug 654')
self._assert_branch_would_be('master/zat', extra_args=['-t', 'zat'])
def test_bug_topic(self):
self._run_git_review('-s')
self._simple_change('a change', 'new change for bug 123')
self._assert_branch_would_be('master/bug/123')
def test_bug_topic_newline(self):
self._run_git_review('-s')
self._simple_change('a change', 'new change not for bug\n\n123')
self._assert_branch_would_be('master')
def test_bp_topic(self):
self._run_git_review('-s')
self._simple_change('a change', 'new change for blueprint asdf')
self._assert_branch_would_be('master/bp/asdf')
def test_bp_topic_newline(self):
self._run_git_review('-s')
self._simple_change('a change', 'new change not for blueprint\n\nasdf')
self._assert_branch_would_be('master')
def test_author_name_topic_bp(self):
old_author = None
if 'GIT_AUTHOR_NAME' in os.environ:
old_author = os.environ['GIT_AUTHOR_NAME']
try:
os.environ['GIT_AUTHOR_NAME'] = 'BPNAME'
self._run_git_review('-s')
self._simple_change('a change',
'new change 1 with name but no topic')
self._assert_branch_would_be('master')
finally:
if old_author:
os.environ['GIT_AUTHOR_NAME'] = old_author
else:
del os.environ['GIT_AUTHOR_NAME']
def test_author_email_topic_bp(self):
old_author = None
if 'GIT_AUTHOR_EMAIL' in os.environ:
old_author = os.environ['GIT_AUTHOR_EMAIL']
try:
os.environ['GIT_AUTHOR_EMAIL'] = 'bpemail@example.com'
self._run_git_review('-s')
self._simple_change('a change',
'new change 1 with email but no topic')
self._assert_branch_would_be('master')
finally:
if old_author:
os.environ['GIT_AUTHOR_EMAIL'] = old_author
else:
del os.environ['GIT_AUTHOR_EMAIL']
def test_author_name_topic_bug(self):
old_author = None
if 'GIT_AUTHOR_NAME' in os.environ:
old_author = os.environ['GIT_AUTHOR_NAME']
try:
os.environ['GIT_AUTHOR_NAME'] = 'Bug: #1234'
self._run_git_review('-s')
self._simple_change('a change',
'new change 2 with name but no topic')
self._assert_branch_would_be('master')
finally:
if old_author:
os.environ['GIT_AUTHOR_NAME'] = old_author
else:
del os.environ['GIT_AUTHOR_NAME']
def test_author_email_topic_bug(self):
old_author = None
if 'GIT_AUTHOR_EMAIL' in os.environ:
old_author = os.environ['GIT_AUTHOR_EMAIL']
try:
os.environ['GIT_AUTHOR_EMAIL'] = 'bug5678@example.com'
self._run_git_review('-s')
self._simple_change('a change',
'new change 2 with email but no topic')
self._assert_branch_would_be('master')
finally:
if old_author:
os.environ['GIT_AUTHOR_EMAIL'] = old_author
else:
del os.environ['GIT_AUTHOR_EMAIL']
def test_git_review_T(self):
self._run_git_review('-s')
self._simple_change('test file modified', 'commit message for bug 456')
self._assert_branch_would_be('master/bug/456')
self._assert_branch_would_be('master', extra_args=['-T'])
def test_git_review_T_t(self):
self.assertRaises(Exception, self._run_git_review, '-T', '-t', 'taz')
def test_git_review_l(self):
self._run_git_review('-s')
# Populate "project" repo
self._simple_change('project: test1', 'project: change1, merged')
self._simple_change('project: test2', 'project: change2, open')
self._simple_change('project: test3', 'project: change3, abandoned')
self._run_git_review('-y')
head = self._run_git('rev-parse', 'HEAD')
head_2 = self._run_git('rev-parse', 'HEAD^^')
self._run_gerrit_cli('review', head_2, '--code-review=+2', '--submit')
self._run_gerrit_cli('review', head, '--abandon')
# Populate "project2" repo
self._run_gerrit_cli('create-project', '--empty-commit', '--name',
'test/test_project2')
project2_uri = self.project_uri.replace('test/test_project',
'test/test_project2')
self._run_git('fetch', project2_uri, 'HEAD')
self._run_git('checkout', 'FETCH_HEAD')
self._simple_change('project2: test1', 'project2: change1, open')
self._run_git('push', project2_uri, 'HEAD:refs/for/master')
# Only project1 open changes
result = self._run_git_review('-l')
self.assertNotIn('project: change1, merged', result)
self.assertIn('project: change2, open', result)
self.assertNotIn('project: change3, abandoned', result)
self.assertNotIn('project2:', result)
def _test_git_review_F(self, rebase):
self._run_git_review('-s')
# Populate repo
self._simple_change('create file', 'test commit message')
change1 = self._run_git('rev-parse', 'HEAD')
self._run_git_review()
self._run_gerrit_cli('review', change1, '--code-review=+2', '--submit')
self._run_git('reset', '--hard', 'HEAD^')
# Review with force_rebase
self._run_git('config', 'gitreview.rebase', rebase)
self._simple_change('create file2', 'test commit message 2',
self._dir('test', 'test_file2.txt'))
self._run_git_review('-F')
head_1 = self._run_git('rev-parse', 'HEAD^')
self.assertEqual(change1, head_1)
def test_git_review_F(self):
self._test_git_review_F('1')
def test_git_review_F_norebase(self):
self._test_git_review_F('0')
def test_git_review_F_R(self):
self.assertRaises(Exception, self._run_git_review, '-F', '-R')
def test_config_instead_of_honored(self):
self.set_remote('test_project_url')
self.assertRaises(Exception, self._run_git_review, '-l')
self._run_git('config', '--add', 'url.%s.insteadof' % self.project_uri,
'test_project_url')
self._run_git_review('-l')
def test_config_pushinsteadof_honored(self):
self.set_remote('test_project_url')
self.assertRaises(Exception, self._run_git_review, '-l')
self._run_git('config', '--add',
'url.%s.pushinsteadof' % self.project_uri,
'test_project_url')
self._run_git_review('-l')
class PushUrlTestCase(GitReviewTestCase):
"""Class for the git-review tests using origin push-url."""
_remote = 'origin'
def set_remote(self, uri):
self._run_git('remote', 'set-url', '--push', self._remote, uri)
def reset_remote(self):
self._run_git('config', '--unset', 'remote.%s.pushurl' % self._remote)
def configure_gerrit_remote(self):
self.set_remote(self.project_uri)
self._run_git('config', 'gitreview.usepushurl', '1')
def test_config_pushinsteadof_honored(self):
self.skipTest("pushinsteadof doesn't rewrite pushurls")
class HttpGitReviewTestCase(tests.HttpMixin, GitReviewTestCase):
"""Class for the git-review tests over HTTP(S)."""
pass

View File

@ -1,296 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
import os
import textwrap
import fixtures
import mock
import testtools
from git_review import cmd
from git_review.tests import utils
# Use of io.StringIO in python =< 2.7 requires all strings handled to be
# unicode. See if StringIO.StringIO is available first
try:
import StringIO as io
except ImportError:
import io
class ConfigTestCase(testtools.TestCase):
"""Class testing config behavior."""
@mock.patch('git_review.cmd.LOCAL_MODE',
mock.PropertyMock(return_value=True))
@mock.patch('git_review.cmd.git_directories', return_value=['', 'fake'])
@mock.patch('git_review.cmd.run_command_exc')
def test_git_local_mode(self, run_mock, dir_mock):
cmd.git_config_get_value('abc', 'def')
run_mock.assert_called_once_with(
cmd.GitConfigException,
'git', 'config', '-f', 'fake/config', '--get', 'abc.def')
@mock.patch('git_review.cmd.LOCAL_MODE',
mock.PropertyMock(return_value=True))
@mock.patch('os.path.exists', return_value=False)
def test_gitreview_local_mode(self, exists_mock):
cmd.Config()
self.assertFalse(exists_mock.called)
class GitReviewConsole(testtools.TestCase, fixtures.TestWithFixtures):
"""Class for testing the console output of git-review."""
reviews = [
{
'number': '1010101',
'branch': 'master',
'subject': 'A simple short subject'
}, {
'number': '9877',
'branch': 'stable/codeword',
'subject': 'A longer and slightly more wordy subject'
}, {
'number': '12345',
'branch': 'master',
'subject': 'A ridiculously long subject that can exceed the '
'normal console width, just need to ensure the '
'max width is short enough'
}]
def setUp(self):
super(GitReviewConsole, self).setUp()
# ensure all tests get a separate git dir to work in to avoid
# local git config from interfering
self.tempdir = self.useFixture(fixtures.TempDir())
self._run_git = functools.partial(utils.run_git,
chdir=self.tempdir.path)
self.run_cmd_patcher = mock.patch('git_review.cmd.run_command_status')
run_cmd_partial = functools.partial(
cmd.run_command_status, GIT_WORK_TREE=self.tempdir.path,
GIT_DIR=os.path.join(self.tempdir.path, '.git'))
self.run_cmd_mock = self.run_cmd_patcher.start()
self.run_cmd_mock.side_effect = run_cmd_partial
self._run_git('init')
self._run_git('commit', '--allow-empty', '-m "initial commit"')
self._run_git('commit', '--allow-empty', '-m "2nd commit"')
def tearDown(self):
self.run_cmd_patcher.stop()
super(GitReviewConsole, self).tearDown()
@mock.patch('git_review.cmd.query_reviews')
@mock.patch('git_review.cmd.get_remote_url', mock.MagicMock)
@mock.patch('git_review.cmd._has_color', False)
def test_list_reviews_no_blanks(self, mock_query):
mock_query.return_value = self.reviews
with mock.patch('sys.stdout', new_callable=io.StringIO) as output:
cmd.list_reviews(None)
console_output = output.getvalue().split('\n')
wrapper = textwrap.TextWrapper(replace_whitespace=False,
drop_whitespace=False)
for text in console_output:
for line in wrapper.wrap(text):
self.assertEqual(line.isspace(), False,
"Extra blank lines appearing between reviews"
"in console output")
@mock.patch('git_review.cmd._use_color', None)
def test_color_output_disabled(self):
"""Test disabling of colour output color.ui defaults to enabled
"""
# git versions < 1.8.4 default to 'color.ui' being false
# so must be set to auto to correctly test
self._run_git("config", "color.ui", "auto")
self._run_git("config", "color.review", "never")
self.assertFalse(cmd.check_use_color_output(),
"Failed to detect color output disabled")
@mock.patch('git_review.cmd._use_color', None)
def test_color_output_forced(self):
"""Test force enable of colour output when color.ui
is defaulted to false
"""
self._run_git("config", "color.ui", "never")
self._run_git("config", "color.review", "always")
self.assertTrue(cmd.check_use_color_output(),
"Failed to detect color output forcefully "
"enabled")
@mock.patch('git_review.cmd._use_color', None)
def test_color_output_fallback(self):
"""Test fallback to using color.ui when color.review is not
set
"""
self._run_git("config", "color.ui", "always")
self.assertTrue(cmd.check_use_color_output(),
"Failed to use fallback to color.ui when "
"color.review not present")
class FakeResponse(object):
def __init__(self, code, text=""):
self.status_code = code
self.text = text
class FakeException(Exception):
def __init__(self, code, *args, **kwargs):
super(FakeException, self).__init__(*args, **kwargs)
self.code = code
FAKE_GIT_CREDENTIAL_FILL = """\
protocol=http
host=gerrit.example.com
username=user
password=pass
"""
class ResolveTrackingUnitTest(testtools.TestCase):
"""Class for testing resolve_tracking."""
def setUp(self):
testtools.TestCase.setUp(self)
patcher = mock.patch('git_review.cmd.run_command_exc')
self.addCleanup(patcher.stop)
self.run_command_exc = patcher.start()
def test_track_local_branch(self):
'Test that local tracked branch is not followed.'
self.run_command_exc.side_effect = [
'',
'refs/heads/other/branch',
]
self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
(u'remote', u'rbranch'))
def test_track_untracked_branch(self):
'Test that local untracked branch is not followed.'
self.run_command_exc.side_effect = [
'',
'',
]
self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
(u'remote', u'rbranch'))
def test_track_remote_branch(self):
'Test that remote tracked branch is followed.'
self.run_command_exc.side_effect = [
'',
'refs/remotes/other/branch',
]
self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
(u'other', u'branch'))
def test_track_git_error(self):
'Test that local tracked branch is not followed.'
self.run_command_exc.side_effect = [cmd.CommandFailed(1, '', [], {})]
self.assertRaises(cmd.CommandFailed,
cmd.resolve_tracking, u'remote', u'rbranch')
class GitReviewUnitTest(testtools.TestCase):
"""Class for misc unit tests."""
@mock.patch('requests.get', return_value=FakeResponse(404))
def test_run_http_exc_raise_http_error(self, mock_get):
url = 'http://gerrit.example.com'
try:
cmd.run_http_exc(FakeException, url)
self.fails('Exception expected')
except FakeException as err:
self.assertEqual(cmd.http_code_2_return_code(404), err.code)
mock_get.assert_called_once_with(url)
@mock.patch('requests.get', side_effect=Exception())
def test_run_http_exc_raise_unknown_error(self, mock_get):
url = 'http://gerrit.example.com'
try:
cmd.run_http_exc(FakeException, url)
self.fails('Exception expected')
except FakeException as err:
self.assertEqual(255, err.code)
mock_get.assert_called_once_with(url)
@mock.patch('git_review.cmd.run_command_exc')
@mock.patch('requests.get', return_value=FakeResponse(200))
def test_run_http_exc_without_auth(self, mock_get, mock_run):
url = 'http://user@gerrit.example.com'
cmd.run_http_exc(FakeException, url)
self.assertFalse(mock_run.called)
mock_get.assert_called_once_with(url)
@mock.patch('git_review.cmd.run_command_exc',
return_value=FAKE_GIT_CREDENTIAL_FILL)
@mock.patch('requests.get',
side_effect=[FakeResponse(401), FakeResponse(200)])
def test_run_http_exc_with_auth(self, mock_get, mock_run):
url = 'http://user@gerrit.example.com'
cmd.run_http_exc(FakeException, url)
mock_run.assert_called_once_with(mock.ANY, 'git', 'credential', 'fill',
stdin='url=%s' % url)
calls = [mock.call(url), mock.call(url, auth=('user', 'pass'))]
mock_get.assert_has_calls(calls)
@mock.patch('git_review.cmd.run_command_exc',
return_value=FAKE_GIT_CREDENTIAL_FILL)
@mock.patch('requests.get', return_value=FakeResponse(401))
def test_run_http_exc_with_failing_auth(self, mock_get, mock_run):
url = 'http://user@gerrit.example.com'
try:
cmd.run_http_exc(FakeException, url)
self.fails('Exception expected')
except FakeException as err:
self.assertEqual(cmd.http_code_2_return_code(401), err.code)
mock_run.assert_called_once_with(mock.ANY, 'git', 'credential', 'fill',
stdin='url=%s' % url)
calls = [mock.call(url), mock.call(url, auth=('user', 'pass'))]
mock_get.assert_has_calls(calls)
@mock.patch('sys.argv', ['argv0', '--track', 'branch'])
@mock.patch('git_review.cmd.check_remote')
@mock.patch('git_review.cmd.resolve_tracking')
def test_command_line_no_track(self, resolve_tracking, check_remote):
check_remote.side_effect = Exception()
self.assertRaises(Exception, cmd._main)
self.assertFalse(resolve_tracking.called)
@mock.patch('sys.argv', ['argv0', '--track'])
@mock.patch('git_review.cmd.check_remote')
@mock.patch('git_review.cmd.resolve_tracking')
def test_track(self, resolve_tracking, check_remote):
check_remote.side_effect = Exception()
self.assertRaises(Exception, cmd._main)
self.assertTrue(resolve_tracking.called)

View File

@ -1,2 +1 @@
argparse argparse
requests>=1.1

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = git-review name = git-restack
summary = Tool to submit code to Gerrit summary = Tool to rebase a git branch
description-file = README.rst description-file = README.rst
license = Apache License (2.0) license = Apache License (2.0)
classifiers = classifiers =
@ -14,19 +14,19 @@ classifiers =
Intended Audience :: Information Technology Intended Audience :: Information Technology
License :: OSI Approved :: Apache Software License License :: OSI Approved :: Apache Software License
Operating System :: OS Independent Operating System :: OS Independent
keywords = git gerrit review keywords = git gerrit restack
author = OpenStack author = OpenStack
author-email = openstack-infra@lists.openstack.org author-email = openstack-infra@lists.openstack.org
home-page = https://docs.openstack.org/infra/git-review/ home-page = https://docs.openstack.org/infra/git-restack/
project-url = https://docs.openstack.org/infra/ project-url = https://docs.openstack.org/infra/
[files] [files]
packages = packages =
git_review git_restack
[entry_points] [entry_points]
console_scripts = console_scripts =
git-review = git_review.cmd:main git-restack = git_restack.cmd:main
[wheel] [wheel]
universal = 1 universal = 1
@ -38,5 +38,5 @@ all_files = 1
[pbr] [pbr]
manpages = manpages =
git-review.1 git-restack.1
warnerrors = True warnerrors = True

View File

@ -1,6 +1,5 @@
hacking>=0.10.0,<0.11 hacking>=0.10.0,<0.11
discover discover
mock
fixtures>=0.3.14 fixtures>=0.3.14
testrepository>=0.0.18 testrepository>=0.0.18
testtools>=0.9.34 testtools>=0.9.34

View File

@ -7,7 +7,6 @@ setenv =
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
commands = commands =
python -m git_review.tests.prepare
python setup.py testr --slowest --testr-args='--concurrency=2 {posargs}' python setup.py testr --slowest --testr-args='--concurrency=2 {posargs}'
deps = deps =