diff --git a/.gitignore b/.gitignore
index e17b175..4c6068c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
build
dist
-git_review.egg-info
+git_restack.egg-info
MANIFEST
AUTHORS
ChangeLog
diff --git a/.gitreview b/.gitreview
index a95fed4..2c51dd2 100644
--- a/.gitreview
+++ b/.gitreview
@@ -1,4 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
-project=openstack-infra/git-review.git
+project=openstack-infra/git-restack.git
diff --git a/.testr.conf b/.testr.conf
index 75c2bce..6e0d94b 100644
--- a/.testr.conf
+++ b/.testr.conf
@@ -2,7 +2,7 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
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_list_option=--list
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 333d221..e2bdcaf 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -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
-Code reviews, as you might expect, are handled by gerrit at:
-https://review.openstack.org
+Code reviews are handled by gerrit at: https://review.openstack.org
See http://wiki.openstack.org/GerritWorkflow for details. Pull
requests submitted through GitHub will be ignored.
diff --git a/HACKING.rst b/HACKING.rst
index e6ccb96..450dc6e 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -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/
Instructions on submitting patches can be found at
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.
OpenStack Style Commandments
diff --git a/MANIFEST.in b/MANIFEST.in
index 21636ab..e0c68e2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,5 +3,5 @@ include LICENSE
include AUTHORS
include ChangeLog
include HACKING.rst
-include git-review.1
+include git-restack.1
include tox.ini
diff --git a/README.rst b/README.rst
index 9861088..4102630 100644
--- a/README.rst
+++ b/README.rst
@@ -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
-review.
+git-restack is a tool that performs an interactive git rebase of a
+branch without changing the commit upon which the branch is based.
* Free software: Apache license
-* Documentation: http://docs.openstack.org/infra/git-review/
-* Source: https://git.openstack.org/cgit/openstack-infra/git-review
-* Bugs: https://storyboard.openstack.org/#!/project/719
+* Documentation: http://docs.openstack.org/infra/git-restack/
+* Source: https://git.openstack.org/cgit/openstack-infra/git-restack
+* Bugs: https://storyboard.openstack.org/#!/project/838
diff --git a/doc/Makefile b/doc/Makefile
index 38f8a49..dcc9f35 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -85,17 +85,17 @@ qthelp:
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".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 "# assistant -collectionFile $(BUILDDIR)/qthelp/git-review.qhc"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/git-restack.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/git-review"
- @echo "# ln -s $(BUILDDIR)/devhelp $$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-restack"
@echo "# devhelp"
epub:
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 42aa408..cb5f703 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,6 +1,6 @@
# -*- 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.
#
# This file is execfile()d with the current directory set to its
@@ -46,7 +46,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
-project = u'git-review'
+project = u'git-restack'
copyright = u'2014, OpenStack Contributors'
# The language for content autogenerated by Sphinx. Refer to documentation
@@ -170,7 +170,7 @@ html_theme = 'default'
#html_file_suffix = None
# Output file base name for HTML help builder.
-htmlhelp_basename = 'git-reviewdoc'
+htmlhelp_basename = 'git-restackdoc'
# -- Options for LaTeX output ---------------------------------------------
@@ -190,7 +190,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- ('index', 'git-review.tex', u'git-review Documentation',
+ ('index', 'git-restack.tex', u'git-restack Documentation',
u'OpenStack Contributors', 'manual'),
]
@@ -220,7 +220,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- ('index', 'git-review', u'git-review Documentation',
+ ('index', 'git-restack', u'git-restack Documentation',
[u'OpenStack Contributors'], 1)
]
@@ -234,8 +234,8 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- ('index', 'git-review', u'git-review Documentation',
- u'OpenStack Contributors', 'git-review', 'One line description of project.',
+ ('index', 'git-restack', u'git-restack Documentation',
+ u'OpenStack Contributors', 'git-restack', 'One line description of project.',
'Miscellaneous'),
]
diff --git a/doc/source/developing.rst b/doc/source/developing.rst
index 9eded60..b8b7c3e 100644
--- a/doc/source/developing.rst
+++ b/doc/source/developing.rst
@@ -2,18 +2,7 @@
Running tests
=============
-
-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::
+To run git-restack tests the following commands may by run::
tox -e py27
tox -e py26
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 7134868..7501523 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -1,9 +1,9 @@
============
- git-review
+ git-restack
============
-``git-review`` is a tool that helps submitting git branches to gerrit
-for review.
+``git-restack`` is a tool that helps edit a series of commits without
+rebasing.
.. toctree::
:maxdepth: 2
diff --git a/doc/source/installation.rst b/doc/source/installation.rst
index b290046..0763508 100644
--- a/doc/source/installation.rst
+++ b/doc/source/installation.rst
@@ -2,89 +2,13 @@
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:
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
-
-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.
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
index 4ab2f0a..0b7679a 100644
--- a/doc/source/usage.rst
+++ b/doc/source/usage.rst
@@ -2,54 +2,11 @@
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
-
-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
+ git restack branchname
diff --git a/git-restack.1 b/git-restack.1
new file mode 100644
index 0000000..e3e921a
--- /dev/null
+++ b/git-restack.1
@@ -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"
diff --git a/git-review.1 b/git-review.1
deleted file mode 100644
index b69c941..0000000
--- a/git-review.1
+++ /dev/null
@@ -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
diff --git a/git_review/__init__.py b/git_restack/__init__.py
similarity index 100%
rename from git_review/__init__.py
rename to git_restack/__init__.py
diff --git a/git_restack/cmd.py b/git_restack/cmd.py
new file mode 100755
index 0000000..bb3a0ed
--- /dev/null
+++ b/git_restack/cmd.py
@@ -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()
diff --git a/git_restack/tests/__init__.py b/git_restack/tests/__init__.py
new file mode 100644
index 0000000..26efb8a
--- /dev/null
+++ b/git_restack/tests/__init__.py
@@ -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
diff --git a/git_restack/tests/test_git_restack.py b/git_restack/tests/test_git_restack.py
new file mode 100644
index 0000000..b8572b0
--- /dev/null
+++ b/git_restack/tests/test_git_restack.py
@@ -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], '')
diff --git a/git_review/tests/utils.py b/git_restack/tests/utils.py
similarity index 98%
rename from git_review/tests/utils.py
rename to git_restack/tests/utils.py
index 337f48a..37bb1bb 100644
--- a/git_review/tests/utils.py
+++ b/git_restack/tests/utils.py
@@ -27,6 +27,7 @@ def run_cmd(*args, **kwargs):
return os.chdir(kwargs['chdir'])
try:
+ os.environ['EDITOR'] = '/bin/cat'
proc = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, env=os.environ,
diff --git a/git_review/cmd.py b/git_review/cmd.py
deleted file mode 100755
index 9351242..0000000
--- a/git_review/cmd.py
+++ /dev/null
@@ -1,1577 +0,0 @@
-#!/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 json
-import os
-import re
-import shlex
-import subprocess
-import sys
-import textwrap
-
-import pkg_resources
-import requests
-
-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(scheme='ssh', hostname=False, port=None, project=False,
- branch='master', remote="gerrit", rebase="1",
- track="0", usepushurl="0")
-
-_branch_name = None
-_has_color = None
-_use_color = None
-_orig_head = None
-_rewrites = None
-_rewrites_push = None
-
-
-class colors(object):
- yellow = '\033[33m'
- green = '\033[92m'
- reset = '\033[0m'
-
-
-class GitReviewException(Exception):
- pass
-
-
-class CommandFailed(GitReviewException):
-
- 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 ChangeSetException(GitReviewException):
-
- def __init__(self, e):
- GitReviewException.__init__(self)
- self.e = str(e)
-
- def __str__(self):
- return self.__doc__ % self.e
-
-
-def printwrap(unwrapped):
- print('\n'.join(textwrap.wrap(unwrapped)))
-
-
-def parse_review_number(review):
- parts = review.split(',')
- if len(parts) < 2:
- parts.append(None)
- return parts
-
-
-def build_review_number(review, patchset):
- if patchset is not None:
- return '%s,%s' % (review, patchset)
- return review
-
-
-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 git_credentials(klazz, url):
- """Get credentials using git credential."""
- cmd = 'git', 'credential', 'fill'
- stdin = 'url=%s' % url
- out = run_command_exc(klazz, *cmd, stdin=stdin)
- data = dict(l.split('=', 1) for l in out.splitlines())
- return data['username'], data['password']
-
-
-def http_code_2_return_code(code):
- """Tranform http status code to system return code."""
- return (code - 301) % 255 + 1
-
-
-def run_http_exc(klazz, url, **env):
- """Run http GET request url, on failure raise klazz
-
- klazz should be derived from CommandFailed
- """
- if url.startswith("https://") and "verify" not in env:
- if "GIT_SSL_NO_VERIFY" in os.environ:
- env["verify"] = False
- else:
- verify = git_config_get_value("http", "sslVerify", as_bool=True)
- env["verify"] = verify != 'false'
-
- try:
- res = requests.get(url, **env)
- if res.status_code == 401:
- env['auth'] = git_credentials(klazz, url)
- res = requests.get(url, **env)
- except klazz:
- raise
- except Exception as err:
- raise klazz(255, str(err), ('GET', url), env)
- if not 200 <= res.status_code < 300:
- raise klazz(http_code_2_return_code(res.status_code),
- res.text, ('GET', url), env)
- return res
-
-
-def get_version():
- requirement = pkg_resources.Requirement.parse('git-review')
- 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, {})
-
-
-class GitDirectoriesException(CommandFailed):
- "Cannot determine where .git directory is."
- EXIT_CODE = 70
-
-
-class CustomScriptException(CommandFailed):
- """Custom script execution failed."""
- EXIT_CODE = 71
-
-
-def run_custom_script(action):
- """Get status and output of .git/hooks/$action-review or/and
- ~/.config/hooks/$action-review if existing.
- """
- returns = []
- script_file = "%s-review" % (action)
- (top_dir, git_dir) = git_directories()
- paths = [os.path.join(CONFIGDIR, "hooks", script_file),
- os.path.join(git_dir, "hooks", script_file)]
- for fpath in paths:
- if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
- status, output = run_command_status(fpath)
- returns.append((status, output, fpath))
-
- for (status, output, path) in returns:
- if status is not None and status != 0:
- raise CustomScriptException(status, output, [path], {})
- elif output and VERBOSE:
- print("script %s output is:" % (path))
- print(output)
-
-
-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
-
-
-class GitConfigException(CommandFailed):
- """Git config value retrieval failed."""
- EXIT_CODE = 128
-
-
-class CannotInstallHook(CommandFailed):
- "Problems encountered installing commit-msg hook"
- EXIT_CODE = 2
-
-
-def set_hooks_commit_msg(remote, target_file):
- """Install the commit message hook if needed."""
-
- # Create the hooks directory if it's not there already
- hooks_dir = os.path.dirname(target_file)
- if not os.path.isdir(hooks_dir):
- os.mkdir(hooks_dir)
-
- if not os.path.exists(target_file) or UPDATE:
- remote_url = get_remote_url(remote)
- if (remote_url.startswith('http://') or
- remote_url.startswith('https://')):
- hook_url = urljoin(remote_url, '/tools/hooks/commit-msg')
- if VERBOSE:
- print("Fetching commit hook from: %s" % hook_url)
- res = run_http_exc(CannotInstallHook, hook_url, stream=True)
- with open(target_file, 'wb') as f:
- for x in res.iter_content(1024):
- f.write(x)
- else:
- (hostname, username, port, project_name) = \
- parse_gerrit_ssh_params_from_git_url(remote_url)
- if username is None:
- userhost = hostname
- else:
- userhost = "%s@%s" % (username, hostname)
- # OS independent target file
- scp_target_file = target_file.replace(os.sep, "/")
- cmd = ["scp", userhost + ":hooks/commit-msg", scp_target_file]
- if port is not None:
- cmd.insert(1, "-P%s" % port)
-
- if VERBOSE:
- hook_url = 'scp://%s%s/hooks/commit-msg' \
- % (userhost, (":%s" % port) if port else "")
- print("Fetching commit hook from: %s" % hook_url)
- run_command_exc(CannotInstallHook, *cmd)
-
- if not os.access(target_file, os.X_OK):
- os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC)
-
-
-def test_remote_url(remote_url):
- """Tests that a possible gerrit remote url works."""
- status, description = run_command_status("git", "push", "--dry-run",
- remote_url, "--all")
- if status != 128:
- if VERBOSE:
- print("%s worked. Description: %s" % (remote_url, description))
- return True
- else:
- if VERBOSE:
- print("%s did not work. Description: %s" % (
- remote_url, description))
- return False
-
-
-def make_remote_url(scheme, username, hostname, port, project):
- """Builds a gerrit remote URL."""
- if port is None and scheme == 'ssh':
- port = 29418
- hostport = '%s:%s' % (hostname, port) if port else hostname
- if username is None:
- return "%s://%s/%s" % (scheme, hostport, project)
- else:
- return "%s://%s@%s/%s" % (scheme, username, hostport, project)
-
-
-def add_remote(scheme, hostname, port, project, remote, usepushurl):
- """Adds a gerrit remote."""
- asked_for_username = False
-
- username = git_config_get_value("gitreview", "username")
- if not username:
- username = os.getenv("USERNAME")
- if not username:
- username = os.getenv("USER")
-
- remote_url = make_remote_url(scheme, username, hostname, port, project)
- if VERBOSE:
- print("No remote set, testing %s" % remote_url)
- if not test_remote_url(remote_url):
- print("Could not connect to gerrit.")
- username = do_input("Enter your gerrit username: ")
- remote_url = make_remote_url(scheme, username, hostname, port, project)
- print("Trying again with %s" % remote_url)
- if not test_remote_url(remote_url):
- raise GitReviewException("Could not connect to gerrit at "
- "%s" % remote_url)
- asked_for_username = True
-
- if usepushurl:
- cmd = "git remote set-url --push %s %s" % (remote, remote_url)
- print("Adding a git push url to '%s' that maps to:" % remote)
- else:
- cmd = "git remote add -f %s %s" % (remote, remote_url)
- print("Creating a git remote called '%s' that maps to:" % remote)
- print("\t%s" % remote_url)
-
- (status, remote_output) = run_command_status(cmd)
- if status != 0:
- raise CommandFailed(status, remote_output, cmd, {})
-
- if asked_for_username:
- print()
- printwrap("This repository is now set up for use with git-review. "
- "You can set the default username for future repositories "
- "with:")
- print(' git config --global --add gitreview.username "%s"' % username)
- print()
-
-
-def populate_rewrites():
- """Populate the global _rewrites and _rewrites_push maps based on the
- output of "git-config".
- """
-
- cmd = ['git', 'config', '--list']
- out = run_command_exc(CommandFailed, *cmd).strip()
-
- global _rewrites, _rewrites_push
- _rewrites = {}
- _rewrites_push = {}
-
- for entry in out.splitlines():
- key, _, value = entry.partition('=')
- key = key.lower()
-
- if key.startswith('url.') and key.endswith('.insteadof'):
- rewrite = key[len('url.'):-len('.insteadof')]
- if rewrite:
- _rewrites[value] = rewrite
- elif key.startswith('url.') and key.endswith('.pushinsteadof'):
- rewrite = key[len('url.'):-len('.pushinsteadof')]
- if rewrite:
- _rewrites_push[value] = rewrite
-
-
-def alias_url(url, rewrite_push):
- """Expand a remote URL. Use the global map _rewrites to replace the
- longest match with its equivalent. If rewrite_push is True, try
- _rewrites_push before _rewrites.
- """
-
- if _rewrites is None:
- populate_rewrites()
-
- if rewrite_push:
- maps = [_rewrites_push, _rewrites]
- else:
- maps = [_rewrites]
-
- for rewrites in maps:
- # If Git finds a pushInsteadOf alias, it uses that even if
- # there is a longer insteadOf alias.
- longest = None
- for alias in rewrites:
- if (url.startswith(alias)
- and (longest is None or len(longest) < len(alias))):
- longest = alias
-
- if longest:
- return url.replace(longest, rewrites[longest])
-
- return url
-
-
-def get_remote_url(remote):
- """Retrieve the remote URL. Read the configuration to expand the URL of a
- remote repository taking into account any "url..insteadOf" or
- "url..pushInsteadOf" config setting.
-
- TODO: Replace current code with something like "git ls-remote
- --get-url" after Git grows a version of it that returns the push
- URL rather than the fetch URL.
- """
-
- push_url = git_config_get_value('remote.%s' % remote, 'pushurl')
- if push_url is not None:
- # Git rewrites pushurl using insteadOf but not pushInsteadOf.
- push_url = alias_url(push_url, False)
- else:
- url = git_config_get_value('remote.%s' % remote, 'url')
- # Git rewrites url using pushInsteadOf or insteadOf.
- push_url = alias_url(url, True)
- if VERBOSE:
- print("Found origin Push URL:", push_url)
- return push_url
-
-
-def parse_gerrit_ssh_params_from_git_url(git_url):
- """Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either
- real URLs or SCP-style addresses.
- """
-
- # The exact code for this in Git itself is a bit obtuse, so just do
- # something sensible and pythonic here instead of copying the exact
- # minutiae from Git.
-
- # Handle real(ish) URLs
- if "://" in git_url:
- parsed_url = urlparse(git_url)
- path = parsed_url.path
-
- hostname = parsed_url.netloc
- username = None
- port = parsed_url.port
-
- # Workaround bug in urlparse on OSX
- if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
- hostname = parsed_url.path[2:].split("/")[0]
-
- if "@" in hostname:
- (username, hostname) = hostname.split("@")
- if ":" in hostname:
- (hostname, port) = hostname.split(":")
-
- if port is not None:
- port = str(port)
-
- # Handle SCP-style addresses
- else:
- username = None
- port = None
- (hostname, path) = git_url.split(":", 1)
- if "@" in hostname:
- (username, hostname) = hostname.split("@", 1)
-
- # Strip leading slash and trailing .git from the path to form the project
- # name.
- project_name = re.sub(r"^/|(\.git$)", "", path)
-
- return (hostname, username, port, project_name)
-
-
-def query_reviews(remote_url, change=None, current_patch_set=True,
- exception=CommandFailed, parse_exc=Exception):
- if remote_url.startswith('http://') or remote_url.startswith('https://'):
- query = query_reviews_over_http
- else:
- query = query_reviews_over_ssh
- return query(remote_url,
- change=change,
- current_patch_set=current_patch_set,
- exception=exception,
- parse_exc=parse_exc)
-
-
-def query_reviews_over_http(remote_url, change=None, current_patch_set=True,
- exception=CommandFailed, parse_exc=Exception):
- url = urljoin(remote_url, '/changes/')
- if change:
- if current_patch_set:
- url += '?q=%s&o=CURRENT_REVISION' % change
- else:
- url += '?q=%s&o=ALL_REVISIONS' % change
- else:
- project_name = re.sub(r"^/|(\.git$)", "", urlparse(remote_url).path)
- params = urlencode({'q': 'project:%s status:open' % project_name})
- url += '?' + params
-
- if VERBOSE:
- print("Query gerrit %s" % url)
- request = run_http_exc(exception, url)
- if VERBOSE:
- print(request.text)
- reviews = json.loads(request.text[4:])
-
- # Reformat output to match ssh output
- try:
- for review in reviews:
- review["number"] = str(review.pop("_number"))
- if "revisions" not in review:
- continue
- patchsets = {}
- for key, revision in review["revisions"].items():
- fetch_value = list(revision["fetch"].values())[0]
- patchset = {"number": str(revision["_number"]),
- "ref": fetch_value["ref"]}
- patchsets[key] = patchset
- review["patchSets"] = patchsets.values()
- review["currentPatchSet"] = patchsets[review["current_revision"]]
- except Exception as err:
- raise parse_exc(err)
-
- return reviews
-
-
-def query_reviews_over_ssh(remote_url, change=None, current_patch_set=True,
- exception=CommandFailed, parse_exc=Exception):
- (hostname, username, port, project_name) = \
- parse_gerrit_ssh_params_from_git_url(remote_url)
-
- if change:
- if current_patch_set:
- query = "--current-patch-set change:%s" % change
- else:
- query = "--patch-sets change:%s" % change
- else:
- query = "project:%s status:open" % project_name
-
- port_data = "p%s" % port if port is not None else ""
- if username is None:
- userhost = hostname
- else:
- userhost = "%s@%s" % (username, hostname)
-
- if VERBOSE:
- print("Query gerrit %s %s" % (remote_url, query))
- output = run_command_exc(
- exception,
- "ssh", "-x" + port_data, userhost,
- "gerrit", "query",
- "--format=JSON %s" % query)
- if VERBOSE:
- print(output)
-
- changes = []
- try:
- for line in output.split("\n"):
- if line[0] == "{":
- try:
- data = json.loads(line)
- if "type" not in data:
- changes.append(data)
- except Exception:
- if VERBOSE:
- print(output)
- except Exception as err:
- raise parse_exc(err)
- return changes
-
-
-def set_color_output(color="auto"):
- global _use_color
- if check_color_support():
- if color == "auto":
- check_use_color_output()
- else:
- _use_color = color == "always"
-
-
-def check_use_color_output():
- global _use_color
- if _use_color is None:
- if check_color_support():
- # we can support color, now check if we should use it
- stdout = "true" if sys.stdout.isatty() else "false"
- test_command = "git config --get-colorbool color.review " + stdout
- color = run_command(test_command)
- _use_color = color == "true"
- else:
- _use_color = False
- return _use_color
-
-
-def check_color_support():
- global _has_color
- if _has_color is None:
- test_command = "git log --color=never --oneline HEAD^1..HEAD"
- (status, output) = run_command_status(test_command)
- if status == 0:
- _has_color = True
- else:
- _has_color = False
- return _has_color
-
-
-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 update_remote(remote):
- cmd = "git remote update %s" % remote
- (status, output) = run_command_status(cmd)
- if VERBOSE:
- print(output)
- if status != 0:
- print("Problem running '%s'" % cmd)
- if not VERBOSE:
- print(output)
- return False
- return True
-
-
-def parse_tracking(ref=None):
- """Return tracked (remote, branch) of current HEAD or other named
- branch if tracking remote.
- """
- if ref is None:
- ref = run_command_exc(
- SymbolicRefFailed,
- "git", "symbolic-ref", "-q", "HEAD")
- tracked = run_command_exc(
- ForEachRefFailed,
- "git", "for-each-ref", "--format=%(upstream)", ref)
-
- # Only on explicitly tracked remote branch do we diverge from default
- if tracked and tracked.startswith('refs/remotes/'):
- return tracked[13:].partition('/')[::2]
-
- return None, None
-
-
-def resolve_tracking(remote, branch):
- """Resolve tracked upstream remote/branch if current branch is tracked."""
- tracked_remote, tracked_branch = parse_tracking()
- # tracked_branch will be empty when tracking a local branch
- if tracked_branch:
- if VERBOSE:
- print('Following tracked %s/%s rather than default %s/%s' % (
- tracked_remote, tracked_branch,
- remote, branch))
- return tracked_remote, tracked_branch
-
- return remote, branch
-
-
-def check_remote(branch, remote, scheme, hostname, port, project,
- usepushurl=False):
- """Check that a Gerrit Git remote repo exists, if not, set one."""
-
- if usepushurl:
- push_url = git_config_get_value('remote.%s' % remote, 'pushurl', None)
- if push_url:
- return
- else:
- has_color = check_color_support()
- if has_color:
- color_never = "--color=never"
- else:
- color_never = ""
-
- if remote in run_command("git remote").split("\n"):
-
- remotes = run_command("git branch -a %s" % color_never).split("\n")
- for current_remote in remotes:
- remote_string = "remotes/%s/%s" % (remote, branch)
- if (current_remote.strip() == remote_string and not UPDATE):
- return
- # We have the remote, but aren't set up to fetch. Fix it
- if VERBOSE:
- print("Setting up gerrit branch tracking for better rebasing")
- update_remote(remote)
- return
-
- if hostname is False or project is False:
- # This means there was no .gitreview file
- printwrap("No '.gitreview' file found in this repository. We don't "
- "know where your gerrit is.")
- if usepushurl:
- printwrap("Please set the push-url on your origin remote to the "
- "location of your gerrit server and try again")
- else:
- printwrap("Please manually create a remote "
- "named \"%s\" and try again." % remote)
- sys.exit(1)
-
- # Gerrit remote not present, try to add it
- try:
- add_remote(scheme, hostname, port, project, remote, usepushurl)
- except Exception:
- print(sys.exc_info()[2])
- if usepushurl:
- printwrap("We don't know where your gerrit is. Please manually"
- " add a push-url to the '%s' remote and try again."
- % remote)
- else:
- printwrap("We don't know where your gerrit is. Please manually"
- " create a remote named '%s' and try again." % remote)
- raise
-
-
-def rebase_changes(branch, remote, interactive=True):
-
- global _orig_head
-
- remote_branch = "remotes/%s/%s" % (remote, branch)
-
- if not update_remote(remote):
- return False
-
- # since the value of ORIG_HEAD may not be set by rebase as expected
- # for use in undo_rebase, make sure to save it explicitly
- cmd = "git rev-parse HEAD"
- (status, output) = run_command_status(cmd)
- if status != 0:
- print("Errors running %s" % cmd)
- if interactive:
- print(output)
- return False
- _orig_head = output
-
- cmd = "git show-ref --quiet --verify refs/%s" % remote_branch
- (status, output) = run_command_status(cmd)
- if status != 0:
- printwrap("The branch '%s' does not exist on the given remote '%s'. "
- "If these changes are intended to start a new branch, "
- "re-run with the '-R' option enabled." % (branch, remote))
- sys.exit(1)
-
- if interactive:
- cmd = "git rebase -p -i %s" % remote_branch
- else:
- cmd = "git rebase -p %s" % remote_branch
-
- (status, output) = run_command_status(cmd, GIT_EDITOR='true')
- if status != 0:
- print("Errors running %s" % cmd)
- if interactive:
- print(output)
- print("It is likely that your change has a merge conflict. "
- "You may resolve it in the working tree now as "
- "described above and then run 'git review' again, or "
- "if you do not want to resolve it yet (note that the "
- "change can not merge until the conflict is resolved) "
- "you may run 'git rebase --abort' then 'git review -R' "
- "to upload the change without rebasing.")
- return False
- return True
-
-
-def undo_rebase():
- global _orig_head
- if not _orig_head:
- return True
-
- cmd = "git reset --hard %s" % _orig_head
- (status, output) = run_command_status(cmd)
- if status != 0:
- print("Errors running %s" % cmd)
- print(output)
- return False
- return True
-
-
-def get_branch_name(target_branch):
- global _branch_name
- if _branch_name is not None:
- return _branch_name
- cmd = "git rev-parse --symbolic-full-name --abbrev-ref HEAD"
- _branch_name = run_command(cmd)
- if _branch_name == "HEAD":
- # detached head or no branch found
- _branch_name = target_branch
- return _branch_name
-
-
-def assert_one_change(remote, branch, yes, have_hook):
- if check_use_color_output():
- use_color = "--color=always"
- else:
- use_color = "--color=never"
- cmd = ("git log %s --decorate --oneline HEAD --not --remotes=%s" % (
- use_color, remote))
- (status, output) = run_command_status(cmd)
- if status != 0:
- print("Had trouble running %s" % cmd)
- print(output)
- sys.exit(1)
- filtered = filter(None, output.split("\n"))
- output_lines = sum(1 for s in filtered)
- if output_lines == 1 and not have_hook:
- printwrap("Your change was committed before the commit hook was "
- "installed. Amending the commit to add a gerrit change id.")
- run_command("git commit --amend", GIT_EDITOR='true')
- elif output_lines == 0:
- printwrap("No changes between HEAD and %s/%s. Submitting for review "
- "would be pointless." % (remote, branch))
- sys.exit(1)
- elif output_lines > 1:
- if not yes:
- printwrap("You are about to submit multiple commits. This is "
- "expected if you are submitting a commit that is "
- "dependent on one or more in-review commits. Otherwise "
- "you should consider squashing your changes into one "
- "commit before submitting.")
- print("\nThe outstanding commits are:\n\n%s\n\n"
- "Do you really want to submit the above commits?" % output)
- yes_no = do_input("Type 'yes' to confirm, other to cancel: ")
- if yes_no.lower().strip() != "yes":
- print("Aborting.")
- sys.exit(1)
-
-
-def use_topic(why, topic):
- """Inform the user about why a particular topic has been selected."""
- if VERBOSE:
- print(why % ('"%s"' % topic,))
- return topic
-
-
-def get_topic(target_branch):
-
- branch_name = get_branch_name(target_branch)
-
- branch_parts = branch_name.split("/")
- if len(branch_parts) >= 3 and branch_parts[0] == "review":
- return use_topic("Using change number %s "
- "for the topic of the change submitted",
- "/".join(branch_parts[2:]))
-
- preferred_log_format = "%B"
- log_output = run_command("git log --pretty='" + preferred_log_format +
- "' HEAD^1..HEAD")
- if log_output == preferred_log_format:
- # The %B format specifier is supported starting at Git v1.7.2. If it's
- # not supported, we'll just get back '%B', so we try something else.
- # The downside of %s is that it removes newlines in the subject.
- log_output = run_command("git log --pretty='%s%n%b' HEAD^1..HEAD")
- bug_re = r'''(?x) # verbose regexp
- \b([Bb]ug|[Ll][Pp]) # bug or lp
- [ \t\f\v]* # don't want to match newline
- [:]? # separator if needed
- [ \t\f\v]* # don't want to match newline
- [#]? # if needed
- [ \t\f\v]* # don't want to match newline
- (\d+) # bug number'''
-
- match = re.search(bug_re, log_output)
- if match is not None:
- return use_topic("Using bug number %s "
- "for the topic of the change submitted",
- "bug/%s" % match.group(2))
-
- bp_re = r'''(?x) # verbose regexp
- \b([Bb]lue[Pp]rint|[Bb][Pp]) # a blueprint or bp
- [ \t\f\v]* # don't want to match newline
- [#:]? # separator if needed
- [ \t\f\v]* # don't want to match newline
- ([0-9a-zA-Z-_]+) # any identifier or number'''
- match = re.search(bp_re, log_output)
- if match is not None:
- return use_topic("Using blueprint number %s "
- "for the topic of the change submitted",
- "bp/%s" % match.group(2))
-
- return use_topic("Using local branch name %s "
- "for the topic of the change submitted",
- branch_name)
-
-
-class CannotQueryOpenChangesets(CommandFailed):
- "Cannot fetch review information from gerrit"
- EXIT_CODE = 32
-
-
-class CannotParseOpenChangesets(ChangeSetException):
- "Cannot parse JSON review information from gerrit"
- EXIT_CODE = 33
-
-
-def list_reviews(remote):
- remote_url = get_remote_url(remote)
- reviews = query_reviews(remote_url,
- exception=CannotQueryOpenChangesets,
- parse_exc=CannotParseOpenChangesets)
-
- if not reviews:
- print("No pending reviews")
- return
-
- REVIEW_FIELDS = ('number', 'branch', 'subject')
- FIELDS = range(len(REVIEW_FIELDS))
- if check_use_color_output():
- review_field_color = (colors.yellow, colors.green, "")
- color_reset = colors.reset
- else:
- review_field_color = ("", "", "")
- color_reset = ""
- review_field_format = ["%*s", "%*s", "%*s"]
- review_field_justify = [+1, +1, -1] # +1 is justify to right
-
- review_list = [[r[f] for f in REVIEW_FIELDS] for r in reviews]
- review_field_width = dict()
- # assume last field is longest and may exceed the console width in which
- # case using the maximum value will result in extra blank lines appearing
- # after each entry even when only one field exceeds the console width
- for i in FIELDS[:-1]:
- review_field_width[i] = max(len(r[i]) for r in review_list)
- review_field_width[len(FIELDS) - 1] = 1
-
- review_field_format = " ".join([
- review_field_color[i] +
- review_field_format[i] +
- color_reset
- for i in FIELDS])
-
- review_field_width = [
- review_field_width[i] * review_field_justify[i]
- for i in FIELDS]
- for review_value in review_list:
- # At this point we have review_field_format
- # like "%*s %*s %*s" and we need to supply
- # (width1, value1, width2, value2, ...) tuple to print
- # It's easy to zip() widths with actual values,
- # but we need to flatten the resulting
- # ((width1, value1), (width2, value2), ...) map.
- formatted_fields = []
- for (width, value) in zip(review_field_width, review_value):
- formatted_fields.extend([width, value.encode('utf-8')])
- print(review_field_format % tuple(formatted_fields))
- print("Found %d items for review" % len(reviews))
-
- return 0
-
-
-class CannotQueryPatchSet(CommandFailed):
- "Cannot query patchset information"
- EXIT_CODE = 34
-
-
-class ReviewInformationNotFound(ChangeSetException):
- "Could not fetch review information for change %s"
- EXIT_CODE = 35
-
-
-class ReviewNotFound(ChangeSetException):
- "Gerrit review %s not found"
- EXIT_CODE = 36
-
-
-class PatchSetGitFetchFailed(CommandFailed):
- """Cannot fetch patchset contents
-
-Does specified change number belong to this project?
-"""
- EXIT_CODE = 37
-
-
-class PatchSetNotFound(ChangeSetException):
- "Review patchset %s not found"
- EXIT_CODE = 38
-
-
-class CheckoutNewBranchFailed(CommandFailed):
- "Cannot checkout to new branch"
- EXIT_CODE = 64
-
-
-class CheckoutExistingBranchFailed(CommandFailed):
- "Cannot checkout existing branch"
- EXIT_CODE = 65
-
-
-class ResetHardFailed(CommandFailed):
- "Failed to hard reset downloaded branch"
- EXIT_CODE = 66
-
-
-class SetUpstreamBranchFailed(CommandFailed):
- "Cannot set upstream to remote branch"
- EXIT_CODE = 67
-
-
-class SymbolicRefFailed(CommandFailed):
- "Cannot find symbolic reference"
- EXIT_CODE = 68
-
-
-class ForEachRefFailed(CommandFailed):
- "Cannot process symbolic reference"
- EXIT_CODE = 69
-
-
-class BranchTrackingMismatch(GitReviewException):
- "Branch exists but is tracking unexpected branch"
- EXIT_CODE = 70
-
-
-def fetch_review(review, masterbranch, remote):
- remote_url = get_remote_url(remote)
-
- review_arg = review
- review, patchset_number = parse_review_number(review)
- current_patch_set = patchset_number is None
-
- review_infos = query_reviews(remote_url,
- change=review,
- current_patch_set=current_patch_set,
- exception=CannotQueryPatchSet,
- parse_exc=ReviewInformationNotFound)
-
- if not len(review_infos):
- raise ReviewInformationNotFound(review)
- review_info = review_infos[0]
-
- try:
- if patchset_number is None:
- refspec = review_info['currentPatchSet']['ref']
- else:
- refspec = [ps for ps in review_info['patchSets']
- if ps['number'] == patchset_number][0]['ref']
- except IndexError:
- raise PatchSetNotFound(review_arg)
- except KeyError:
- raise ReviewNotFound(review)
-
- try:
- topic = review_info['topic']
- if topic == masterbranch:
- topic = review
- except KeyError:
- topic = review
- try:
- author = re.sub('\W+', '_', review_info['owner']['name']).lower()
- except KeyError:
- author = 'unknown'
- remote_branch = review_info['branch']
-
- if patchset_number is None:
- branch_name = "review/%s/%s" % (author, topic)
- else:
- branch_name = "review/%s/%s-patch%s" % (author, topic, patchset_number)
-
- print("Downloading %s from gerrit" % refspec)
- run_command_exc(PatchSetGitFetchFailed,
- "git", "fetch", remote, refspec)
- return branch_name, remote_branch
-
-
-def checkout_review(branch_name, remote, remote_branch):
- """Checkout a newly fetched (FETCH_HEAD) change
- into a branch
- """
-
- try:
- run_command_exc(CheckoutNewBranchFailed,
- "git", "checkout", "-b",
- branch_name, "FETCH_HEAD")
- # --set-upstream-to is not supported in git 1.7
- run_command_exc(SetUpstreamBranchFailed,
- "git", "branch", "--set-upstream",
- branch_name,
- '%s/%s' % (remote, remote_branch))
-
- except CheckoutNewBranchFailed as e:
- if re.search("already exists\.?", e.output):
- print("Branch %s already exists - reusing" % branch_name)
- track_remote, track_branch = parse_tracking(
- ref='refs/heads/' + branch_name)
- if track_remote and not (track_remote == remote and
- track_branch == remote_branch):
- print("Branch %s incorrectly tracking %s/%s instead of %s/%s"
- % (branch_name,
- track_remote, track_branch,
- remote, remote_branch))
- raise BranchTrackingMismatch
- run_command_exc(CheckoutExistingBranchFailed,
- "git", "checkout", branch_name)
- run_command_exc(ResetHardFailed,
- "git", "reset", "--hard", "FETCH_HEAD")
- else:
- raise
-
- print("Switched to branch \"%s\"" % branch_name)
-
-
-class PatchSetGitCherrypickFailed(CommandFailed):
- "There was a problem applying changeset contents to the current branch."
- EXIT_CODE = 69
-
-
-def cherrypick_review(option=None):
- cmd = ["git", "cherry-pick"]
- if option:
- cmd.append(option)
- cmd.append("FETCH_HEAD")
- print(run_command_exc(PatchSetGitCherrypickFailed, *cmd))
-
-
-class CheckoutBackExistingBranchFailed(CommandFailed):
- "Cannot switch back to existing branch"
- EXIT_CODE = 67
-
-
-class DeleteBranchFailed(CommandFailed):
- "Failed to delete branch"
- EXIT_CODE = 68
-
-
-class InvalidPatchsetsToCompare(GitReviewException):
- def __init__(self, patchsetA, patchsetB):
- Exception.__init__(
- self,
- "Invalid patchsets for comparison specified (old=%s,new=%s)" % (
- patchsetA,
- patchsetB))
- EXIT_CODE = 39
-
-
-def compare_review(review_spec, branch, remote, rebase=False):
- new_ps = None # none means latest
-
- if '-' in review_spec:
- review_spec, new_ps = review_spec.split('-')
- review, old_ps = parse_review_number(review_spec)
-
- if old_ps is None or old_ps == new_ps:
- raise InvalidPatchsetsToCompare(old_ps, new_ps)
-
- old_review = build_review_number(review, old_ps)
- new_review = build_review_number(review, new_ps)
-
- old_branch, _ = fetch_review(old_review, branch, remote)
- checkout_review(old_branch, None, None)
-
- if rebase:
- print('Rebasing %s' % old_branch)
- rebase = rebase_changes(branch, remote, False)
- if not rebase:
- print('Skipping rebase because of conflicts')
- run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
-
- new_branch, remote_branch = fetch_review(new_review, branch, remote)
- checkout_review(new_branch, remote, remote_branch)
-
- if rebase:
- print('Rebasing also %s' % new_branch)
- if not rebase_changes(branch, remote, False):
- print("Rebasing of the new branch failed, "
- "diff can be messed up (use -R to not rebase at all)!")
- run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
-
- subprocess.check_call(['git', 'diff', old_branch])
-
-
-def finish_branch(target_branch):
- local_branch = get_branch_name(target_branch)
- if VERBOSE:
- print("Switching back to '%s' and deleting '%s'" % (target_branch,
- local_branch))
- run_command_exc(CheckoutBackExistingBranchFailed,
- "git", "checkout", target_branch)
- print("Switched to branch '%s'" % target_branch)
-
- run_command_exc(DeleteBranchFailed,
- "git", "branch", "-D", local_branch)
- print("Deleted branch '%s'" % local_branch)
-
-
-def convert_bool(one_or_zero):
- "Return a bool on a one or zero string."
- return str(one_or_zero) in ["1", "true", "True"]
-
-
-class MalformedInput(GitReviewException):
- EXIT_CODE = 3
-
-
-def assert_valid_reviewers(reviewers):
- """Ensure no whitespace is found in reviewer names, as it will result
- in an invalid refspec.
- """
- for reviewer in reviewers:
- if re.search(r'\s', reviewer):
- raise MalformedInput(
- "Whitespace not allowed in reviewer: '%s'" % reviewer)
-
-
-def _main():
- usage = "git review [OPTIONS] ... [BRANCH]"
-
- class DownloadFlag(argparse.Action):
- """Additional option parsing: store value in 'dest', but
- at the same time set one of the flag options to True
- """
- def __call__(self, parser, namespace, values, option_string=None):
- setattr(namespace, self.dest, values)
- setattr(namespace, self.const, True)
-
- parser = argparse.ArgumentParser(usage=usage, description=COPYRIGHT)
-
- topic_arg_group = parser.add_mutually_exclusive_group()
- topic_arg_group.add_argument("-t", "--topic", dest="topic",
- help="Topic to submit branch to")
- topic_arg_group.add_argument("-T", "--no-topic", dest="notopic",
- action="store_true",
- help="No topic except if explicitly provided")
-
- parser.add_argument("--reviewers", nargs="+",
- help="Add reviewers to uploaded patch sets.")
- parser.add_argument("-D", "--draft", dest="draft", action="store_true",
- help="Submit review as a draft")
- parser.add_argument("-c", "--compatible", dest="compatible",
- action="store_true",
- help="Push change to refs/for/* for compatibility "
- "with Gerrit versions < 2.3. Ignored if "
- "-D/--draft is used.")
- parser.add_argument("-n", "--dry-run", dest="dry", action="store_true",
- help="Don't actually submit the branch for review")
- parser.add_argument("-i", "--new-changeid", dest="regenerate",
- action="store_true",
- help="Regenerate Change-id before submitting")
- parser.add_argument("-r", "--remote", dest="remote",
- help="git remote to use for gerrit")
- parser.add_argument("--use-pushurl", dest="usepushurl",
- action="store_true",
- help="Use remote push-url logic instead of separate"
- " remotes")
-
- rebase_group = parser.add_mutually_exclusive_group()
- rebase_group.add_argument("-R", "--no-rebase", dest="rebase",
- action="store_false",
- help="Don't rebase changes before submitting.")
- rebase_group.add_argument("-F", "--force-rebase", dest="force_rebase",
- action="store_true",
- help="Force rebase even when not needed.")
-
- track_group = parser.add_mutually_exclusive_group()
- track_group.add_argument("--track", dest="track",
- action="store_true",
- help="Use tracked branch as default.")
- track_group.add_argument("--no-track", dest="track",
- action="store_false",
- help="Ignore tracked branch.")
-
- fetch = parser.add_mutually_exclusive_group()
- fetch.set_defaults(download=False, compare=False, cherrypickcommit=False,
- cherrypickindicate=False, cherrypickonly=False)
- fetch.add_argument("-d", "--download", dest="changeidentifier",
- action=DownloadFlag, metavar="CHANGE",
- const="download",
- help="Download the contents of an existing gerrit "
- "review into a branch")
- fetch.add_argument("-x", "--cherrypick", dest="changeidentifier",
- action=DownloadFlag, metavar="CHANGE",
- const="cherrypickcommit",
- help="Apply the contents of an existing gerrit "
- "review onto the current branch and commit "
- "(cherry pick; not recommended in most "
- "situations)")
- fetch.add_argument("-X", "--cherrypickindicate", dest="changeidentifier",
- action=DownloadFlag, metavar="CHANGE",
- const="cherrypickindicate",
- help="Apply the contents of an existing gerrit "
- "review onto the current branch and commit, "
- "indicating its origin")
- fetch.add_argument("-N", "--cherrypickonly", dest="changeidentifier",
- action=DownloadFlag, metavar="CHANGE",
- const="cherrypickonly",
- help="Apply the contents of an existing gerrit "
- "review to the working directory and prepare "
- "for commit")
- fetch.add_argument("-m", "--compare", dest="changeidentifier",
- action=DownloadFlag, metavar="CHANGE,PS[-NEW_PS]",
- const="compare",
- help="Download specified and latest (or NEW_PS) "
- "patchsets of an existing gerrit review into "
- "a branches, rebase on master "
- "(skipped on conflicts or when -R is specified) "
- "and show their differences")
-
- parser.add_argument("-u", "--update", dest="update", action="store_true",
- help="Force updates from remote locations")
- parser.add_argument("-s", "--setup", dest="setup", action="store_true",
- help="Just run the repo setup commands but don't "
- "submit anything")
- parser.add_argument("-f", "--finish", dest="finish", action="store_true",
- help="Close down this branch and switch back to "
- "master on successful submission")
- parser.add_argument("-l", "--list", dest="list", action="store_true",
- help="List available reviews for the current project")
- parser.add_argument("-y", "--yes", dest="yes", action="store_true",
- help="Indicate that you do, in fact, understand if "
- "you are submitting more than one patch")
- parser.add_argument("-v", "--verbose", dest="verbose", action="store_true",
- help="Output more information about what's going on")
- parser.add_argument("--no-custom-script", dest="custom_script",
- action="store_false", default=True,
- help="Do not run custom scripts.")
- parser.add_argument("--color", dest="color", metavar="",
- nargs="?", choices=["always", "never", "auto"],
- help="Show color output. --color (without []) "
- "is the same as --color=always. can be "
- "one of %(choices)s. Behaviour can also be "
- "controlled by the color.ui and color.review "
- "configuration settings.")
- parser.add_argument("--no-color", dest="color", action="store_const",
- const="never",
- help="Turn off colored output. Can be used to "
- "override configuration options. Same as "
- "setting --color=never.")
- 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(dry=False,
- draft=False,
- verbose=False,
- update=False,
- setup=False,
- list=False,
- yes=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"))
- parser.set_defaults(rebase=convert_bool(config['rebase']),
- track=convert_bool(config['track']),
- remote=None,
- usepushurl=convert_bool(config['usepushurl']))
- options = parser.parse_args()
- if no_git_dir:
- raise no_git_dir
-
- if options.license:
- print(COPYRIGHT)
- sys.exit(0)
-
- if options.branch is None:
- branch = config['branch']
- else:
- # explicitly-specified branch on command line overrides options.track
- branch = options.branch
- options.track = False
-
- global VERBOSE
- global UPDATE
- VERBOSE = options.verbose
- UPDATE = options.update
- remote = options.remote
- if not remote:
- if options.usepushurl:
- remote = 'origin'
- else:
- remote = config['remote']
- yes = options.yes
- status = 0
-
- if options.track:
- remote, branch = resolve_tracking(remote, branch)
-
- check_remote(branch, remote, config['scheme'],
- config['hostname'], config['port'], config['project'],
- usepushurl=options.usepushurl)
-
- if options.color:
- set_color_output(options.color)
-
- if options.changeidentifier:
- if options.compare:
- compare_review(options.changeidentifier,
- branch, remote, options.rebase)
- return
- local_branch, remote_branch = fetch_review(options.changeidentifier,
- branch, remote)
- if options.download:
- checkout_review(local_branch, remote, remote_branch)
- else:
- if options.cherrypickcommit:
- cherrypick_review()
- elif options.cherrypickonly:
- cherrypick_review("-n")
- if options.cherrypickindicate:
- cherrypick_review("-x")
- return
- elif options.list:
- list_reviews(remote)
- return
-
- if options.custom_script:
- run_custom_script("pre")
-
- hook_file = os.path.join(git_dir, "hooks", "commit-msg")
- have_hook = os.path.exists(hook_file) and os.access(hook_file, os.X_OK)
-
- if not have_hook:
- set_hooks_commit_msg(remote, hook_file)
-
- if options.setup:
- if options.finish and not options.dry:
- finish_branch(branch)
- return
-
- if options.rebase or options.force_rebase:
- if not rebase_changes(branch, remote):
- sys.exit(1)
- if not options.force_rebase and not undo_rebase():
- sys.exit(1)
- assert_one_change(remote, branch, yes, have_hook)
-
- ref = "publish"
-
- if options.draft:
- ref = "drafts"
- if options.custom_script:
- run_custom_script("draft")
- elif options.compatible:
- ref = "for"
-
- cmd = "git push %s HEAD:refs/%s/%s" % (remote, ref, branch)
- if options.topic is not None:
- topic = options.topic
- else:
- topic = None if options.notopic else get_topic(branch)
- if topic and topic != branch:
- cmd += "/%s" % topic
-
- if options.reviewers:
- assert_valid_reviewers(options.reviewers)
- cmd += "%" + ",".join("r=%s" % r for r in options.reviewers)
-
- if options.regenerate:
- print("Amending the commit to regenerate the change id\n")
- regenerate_cmd = "git commit --amend"
- if options.dry:
- print("\tGIT_EDITOR=\"sed -i -e '/^Change-Id:/d'\" %s\n" %
- regenerate_cmd)
- else:
- run_command(regenerate_cmd,
- GIT_EDITOR="sed -i -e "
- "'/^Change-Id:/d'")
-
- if options.dry:
- print("Please use the following command "
- "to send your commits to review:\n")
- print("\t%s\n" % cmd)
- else:
- (status, output) = run_command_status(cmd)
- print(output)
-
- if options.finish and not options.dry and status == 0:
- finish_branch(branch)
- return
-
- if options.custom_script:
- run_custom_script("post")
- sys.exit(status)
-
-
-def main():
- try:
- _main()
- except GitReviewException as e:
- # If one does unguarded print(e) here, in certain locales the implicit
- # str(e) blows up with familiar "UnicodeEncodeError ... ordinal not in
- # range(128)". See rhbz#1058167.
- try:
- u = unicode(e)
- except NameError:
- # Python 3, we're home free.
- print(e)
- else:
- print(u.encode('utf-8'))
- sys.exit(e.EXIT_CODE)
-
-
-if __name__ == "__main__":
- main()
diff --git a/git_review/tests/__init__.py b/git_review/tests/__init__.py
deleted file mode 100644
index ef1fb0d..0000000
--- a/git_review/tests/__init__.py
+++ /dev/null
@@ -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')
diff --git a/git_review/tests/prepare.py b/git_review/tests/prepare.py
deleted file mode 100644
index 089f941..0000000
--- a/git_review/tests/prepare.py
+++ /dev/null
@@ -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()
diff --git a/git_review/tests/test_git_review.py b/git_review/tests/test_git_review.py
deleted file mode 100644
index 259aec1..0000000
--- a/git_review/tests/test_git_review.py
+++ /dev/null
@@ -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
diff --git a/git_review/tests/test_unit.py b/git_review/tests/test_unit.py
deleted file mode 100644
index 05113fe..0000000
--- a/git_review/tests/test_unit.py
+++ /dev/null
@@ -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)
diff --git a/requirements.txt b/requirements.txt
index e28f5fb..1352d5e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
argparse
-requests>=1.1
diff --git a/setup.cfg b/setup.cfg
index 3aa6f2b..1bc5b26 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
-name = git-review
-summary = Tool to submit code to Gerrit
+name = git-restack
+summary = Tool to rebase a git branch
description-file = README.rst
license = Apache License (2.0)
classifiers =
@@ -14,19 +14,19 @@ classifiers =
Intended Audience :: Information Technology
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
-keywords = git gerrit review
+keywords = git gerrit restack
author = OpenStack
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/
[files]
packages =
- git_review
+ git_restack
[entry_points]
console_scripts =
- git-review = git_review.cmd:main
+ git-restack = git_restack.cmd:main
[wheel]
universal = 1
@@ -38,5 +38,5 @@ all_files = 1
[pbr]
manpages =
- git-review.1
+ git-restack.1
warnerrors = True
diff --git a/test-requirements.txt b/test-requirements.txt
index 085a45e..702869a 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,5 @@
hacking>=0.10.0,<0.11
discover
-mock
fixtures>=0.3.14
testrepository>=0.0.18
testtools>=0.9.34
diff --git a/tox.ini b/tox.ini
index 59bec38..efe767f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,6 @@ setenv =
VIRTUAL_ENV={envdir}
commands =
- python -m git_review.tests.prepare
python setup.py testr --slowest --testr-args='--concurrency=2 {posargs}'
deps =