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 =