├── README.md ├── git-pull-request ├── git-pull-request.bat ├── git-pull-request.sh ├── README.md └── git-pull-request.py ├── git-getm ├── git-sync-origin ├── git-of-interest ├── git-new-workdir ├── git-new-workdir.cmd └── gitconfig /README.md: -------------------------------------------------------------------------------- 1 | # Git Tools 2 | 3 | This repository is a collection of scripts and other tools that the developers of Liferay 4 | use to help speed up their git workflow. -------------------------------------------------------------------------------- /git-pull-request/git-pull-request.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Add an alias for this script to your ".gitconfig" (use your own path): 4 | rem 5 | rem [alias] 6 | rem pr = !c:/projects/git-tools/git-pull-request/git-pull-request.bat 7 | rem 8 | rem Run the script as: "git pr" 9 | 10 | "%~dp0\git-pull-request.py" %* -------------------------------------------------------------------------------- /git-pull-request/git-pull-request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Add an alias for this script to your bash profile as follows: 4 | # alias gitpr="source YOUR_DIRECTORY/git-pull-request/git-pull-request.sh" 5 | 6 | > /tmp/git-pull-request-chdir 7 | 8 | PR=`dirname "$BASH_SOURCE"` 9 | "$PR/git-pull-request.py" "$@" 10 | 11 | DIR=`cat /tmp/git-pull-request-chdir` 12 | 13 | if [ -n "$DIR" ]; then 14 | cd $DIR 15 | fi 16 | -------------------------------------------------------------------------------- /git-getm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cur_dir=$(pwd); 4 | branch_name="master"; 5 | 6 | if [[ $cur_dir == *-ee-* ]]; then 7 | repo_path=$(echo $cur_dir | sed -E 's#-ee-([^/]+)/.*#-ee-\1#') 8 | cur_dir="${cur_dir#*ee-}" 9 | cur_dir="ee-${cur_dir%%/*}" 10 | git show-ref --quiet "$cur_dir" && branch_name="$cur_dir" 11 | [[ -f "$repo_path/.git/refs/heads/$cur_dir" ]] && branch_name="$cur_dir" 12 | fi; 13 | 14 | echo $branch_name -------------------------------------------------------------------------------- /git-sync-origin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | old_head=$(git rev-parse --short head | tr -d '\n') 4 | 5 | msg=$(git stash save) 6 | 7 | [[ $msg =~ ^'No local changes to save'$ ]] && stashed=0 || stashed=1 8 | 9 | git fetch upstream && git merge upstream/$1 --ff-only && git push origin $1 10 | 11 | new_head=$(git rev-parse --short head | tr -d '\n') 12 | 13 | [ $old_head != $new_head ] && updated=1 || updated=0 14 | 15 | if [[ $stashed == 1 ]]; then 16 | git stash pop > /dev/null 17 | fi 18 | 19 | if [[ $updated == 1 ]]; then 20 | echo "Updated from $old_head to $new_head ($old_head..$new_head)" 21 | echo "---------------------------------------------------" 22 | 23 | git of-interest $old_head..$new_head 24 | 25 | echo "---------------------------------------------------" 26 | echo "Updated from $old_head to $new_head ($old_head..$new_head)" 27 | fi -------------------------------------------------------------------------------- /git-of-interest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | file_types=${2:-"*.js *.css *.jsp* *.vm *.ftl"} 4 | 5 | ignore_folder="portal-web/test/" 6 | 7 | to_rev=${1:-"HEAD"}; 8 | old_head=${to_rev}^; 9 | new_head=$to_rev; 10 | 11 | if [[ $to_rev == *..* ]]; then 12 | old_head=${to_rev%%..*}; 13 | new_head=${to_rev#*..*}; 14 | fi 15 | 16 | ref_spec=$old_head..$new_head; 17 | 18 | entries=$(git log -M -C --name-only --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(cyan)<%an>%Creset' --abbrev-commit --date=relative $ref_spec -- $file_types | sed 's/^/ /') 19 | 20 | totalstat=$(git diff --numstat --pretty='%H' --no-renames $ref_spec | grep -v "\b$ignore_folder" | xargs -0n1 echo -n | awk '{{print $3}}' | sed -e 's/^.*\.\(.*\)$/\1/' | sort | uniq -c | tr '\n' ',' | sed 's/,$//') 21 | 22 | if [[ -z $entries ]]; then 23 | echo "There are no changes in ${file_types// /, } across $ref_spec" 24 | else 25 | echo "Changes in these file types: ${file_types// /, } that you might be interested in:" 26 | 27 | echo "$entries" 28 | echo "---" 29 | echo "$totalstat" 30 | fi -------------------------------------------------------------------------------- /git-pull-request/README.md: -------------------------------------------------------------------------------- 1 | # Git Pull Request 2 | 3 | Git command to automate many common tasks involving pull requests. 4 | 5 | Based on scripts by [Connor McKay](connor.mckay@liferay.com), [Andreas Gohr](andi@splitbrain.org), [Minhchau Dang](minhchau.dang@liferay.com) and [Nate Cavanaugh](nathan.cavanaugh@liferay.com). 6 | 7 | ## Install 8 | 9 | 1. First clone this repository to a directory of your choice. 10 | 11 | $ git clone git://github.com/liferay/git-tools.git 12 | 13 | 2. Then edit your bash profile and add the following line: 14 | 15 | alias gitpr="source YOUR_DIRECTORY/git-tools/git-pull-request/git-pull-request.sh" 16 | 17 | 3. Go to to find your API token. Then edit your `.gitconfig` file and add the following: 18 | 19 | [github] 20 | user = your github username 21 | token = your github API token 22 | 23 | 4. Change into a local git repository and try it out! To see a list of all open pull requests on your repository, run: 24 | 25 | gitpr 26 | 27 | To see a list of all possible commands, run: 28 | 29 | gitpr help 30 | 31 | 5. If you want to use the "user aliases" functionality you need to configure your git repo with: 32 | 33 | git config git-pull-request.users-alias-file PATH_TO_YOUR_FILE (local to the current repo) 34 | 35 | git config --global git-pull-request.users-alias-file PATH_TO_YOUR_FILE (global for all the git repos) 36 | 37 | Run the command gitpr update-users. This command will populate the previous file with all the info of the users who has forked your upstream repository -------------------------------------------------------------------------------- /git-new-workdir: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | echo "usage:" $@ 5 | exit 127 6 | } 7 | 8 | die () { 9 | echo $@ 10 | exit 128 11 | } 12 | 13 | if test $# -lt 2 || test $# -gt 3 14 | then 15 | usage "$0 []" 16 | fi 17 | 18 | orig_git=$1 19 | new_workdir=$2 20 | branch=$3 21 | 22 | # want to make sure that what is pointed to has a .git directory ... 23 | git_dir=$(cd "$orig_git" 2>/dev/null && 24 | git rev-parse --git-dir 2>/dev/null) || 25 | die "Not a git repository: \"$orig_git\"" 26 | 27 | case "$git_dir" in 28 | .git) 29 | git_dir="$orig_git/.git" 30 | ;; 31 | .) 32 | git_dir=$orig_git 33 | ;; 34 | esac 35 | 36 | # don't link to a configured bare repository 37 | isbare=$(git --git-dir="$git_dir" config --bool --get core.bare) 38 | if test ztrue = z$isbare 39 | then 40 | die "\"$git_dir\" has core.bare set to true," \ 41 | " remove from \"$git_dir/config\" to use $0" 42 | fi 43 | 44 | # don't link to a workdir 45 | if test -h "$git_dir/config" 46 | then 47 | die "\"$orig_git\" is a working directory only, please specify" \ 48 | "a complete repository." 49 | fi 50 | 51 | # don't recreate a workdir over an existing repository 52 | if test -e "$new_workdir" 53 | then 54 | die "destination directory '$new_workdir' already exists." 55 | fi 56 | 57 | # make sure the links use full paths 58 | git_dir=$(cd "$git_dir"; pwd) 59 | 60 | # create the workdir 61 | mkdir -p "$new_workdir/.git" || die "unable to create \"$new_workdir\"!" 62 | 63 | # create the links to the original repo. explicitly exclude index, HEAD and 64 | # logs/HEAD from the list since they are purely related to the current working 65 | # directory, and should not be shared. 66 | for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache svn 67 | do 68 | case $x in 69 | */*) 70 | mkdir -p "$(dirname "$new_workdir/.git/$x")" 71 | ;; 72 | esac 73 | ln -s "$git_dir/$x" "$new_workdir/.git/$x" 74 | done 75 | 76 | # now setup the workdir 77 | cd "$new_workdir" 78 | # copy the HEAD from the original repository as a default branch 79 | cp "$git_dir/HEAD" .git/HEAD 80 | # checkout the branch (either the same as HEAD from the original repository, or 81 | # the one that was asked for) 82 | git checkout -f $branch 83 | -------------------------------------------------------------------------------- /git-new-workdir.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Create a GIT working directory for an existing repository 4 | REM %1 - Path of the existing repository 5 | REM %2 - Path of the new working directory 6 | REM %3 - Branch (optional) 7 | 8 | IF #%1# == ## GOTO usage 9 | IF #%2# == ## GOTO usage 10 | 11 | SET GITCMD="%COMSPEC%" /c git 12 | SET BRANCH=%3 13 | 14 | REM Get source and destination folder 15 | FOR %%d IN (%1) DO SET SRC=%%~fd 16 | FOR %%d IN (%2) DO SET DST=%%~fd 17 | 18 | REM Want to make sure that what is pointed to has a .git directory ... 19 | PUSHD "%SRC%" 2>NUL 20 | IF ERRORLEVEL 1 GOTO error_no_source 21 | 22 | %GITCMD% rev-parse --git-dir 2>&1 >NUL 23 | IF ERRORLEVEL 1 GOTO error_no_repository 24 | 25 | REM Get full path to .git directory 26 | FOR /F %%d IN ('%GITCMD% rev-parse --git-dir') DO SET GITDIR=%%~fd 27 | POPD 28 | 29 | REM Get a short name to prevent spaces 30 | FOR %%f in ("%GITDIR%") DO SET GITSHORTDIR=%%~fsf 31 | 32 | REM Don't link to a configured bare repository 33 | FOR /F %%d IN ('%GITCMD% --git-dir=%GITSHORTDIR% config --bool --get core.bare') DO SET IS_BARE=%%d 34 | 35 | IF #%IS_BARE%# == #true# GOTO error_bare 36 | 37 | REM TODO: Check if the source is a working copy 38 | 39 | REM Do not overwrite existing directories 40 | IF EXIST "%DST%" GOTO error_destination_exists 41 | 42 | REM Create the workdir and the logs sub dir 43 | MKDIR "%DST%\.git\logs" 44 | IF ERRORLEVEL 1 GOTO error_create_workdir 45 | 46 | REM create the links to the original repo. explicitly exclude index, HEAD and 47 | REM logs/HEAD from the list since they are purely related to the current working 48 | REM directory, and should not be shared. 49 | 50 | REM Directories 51 | FOR %%x in (refs logs\refs objects info hooks remotes rr-cache svn) DO ( 52 | IF EXIST "%GITDIR%\%%x" ( 53 | mklink /D "%DST%\.git\%%x" "%GITDIR%\%%x" 2>&1 >NUL 54 | ) 55 | ) 56 | 57 | REM Files 58 | FOR %%x in (config packed-refs) DO ( 59 | IF EXIST "%GITDIR%\%%x" ( 60 | mklink "%DST%\.git\%%x" "%GITDIR%\%%x" 2>&1 >NUL 61 | ) 62 | ) 63 | 64 | REM Now setup the workdir 65 | PUSHD "%DST%" 66 | 67 | REM Copy the HEAD from the original repository as a default branch 68 | COPY "%GITDIR%\HEAD" .git\HEAD >NUL 69 | 70 | REM Checkout the branch (either the same as HEAD from the original repository, or 71 | REM the one that was asked for) 72 | %GITCMD% checkout -f %BRANCH% 73 | 74 | ECHO Created work dir in "%DST%" 75 | POPD 76 | 77 | REM That's it 78 | EXIT /B 0 79 | 80 | REM ---- Error messages ---- 81 | 82 | :usage 83 | ECHO Usage: %0 ^ ^ [^] 84 | EXIT /B 127 85 | 86 | :error_no_source 87 | ECHO Directory not found: "%SRC%" 88 | EXIT /B 128 89 | 90 | :error_no_repository 91 | ECHO Not a git repository: "%SRC%" 92 | POPD 93 | EXIT /B 128 94 | 95 | :error_bare 96 | ECHO "%SRC%" is a bare repository. 97 | EXIT /B 128 98 | 99 | :error_destination_exists 100 | ECHO Destination directory "%DST%" already exists 101 | EXIT /B 128 102 | 103 | :error_create_workdir 104 | ECHO Unable to create "%DST%"! 105 | EXIT /B 128 -------------------------------------------------------------------------------- /gitconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | quotepath = false 3 | autocrlf = input 4 | [color] 5 | ui = auto 6 | [alias] 7 | # @Requires the following scripts (all available in this repo): 8 | # git-get-latest-jira-id 9 | # get-getm 10 | # git-sync-origin 11 | # git-of-interest 12 | # git-pull-request 13 | 14 | ## Committing 15 | ##-------- 16 | 17 | # Commit 18 | ci = commit 19 | # Commit w/ message 20 | cim = commit -m 21 | # Amend commit 22 | cia = commit --amend 23 | # Amend commit w/message 24 | ciam = commit --amend -m 25 | # Add changed files and then commit w/message 26 | cam = commit -a -m 27 | # Commit and add files 28 | ca = commit -a 29 | # These are the same as cam, ca and squish, except it adds new files as well 30 | acam = !git add -A && git commit -m 31 | aca = !git add -A && git commit 32 | asquish = !git add -A && git commit --amend -C HEAD 33 | # Adds all changed files into the last commit 34 | squish = commit -a --amend -C HEAD 35 | # Adds only staged files into the last commit 36 | squeeze = commit --amend -C HEAD 37 | 38 | # camm and cimm, acts as cam and cim aliases 39 | # but automatically creates a message using the last JIRA ticket it can find (if any) 40 | # can take one optional parameter which is the message to use after the 41 | # ticket ID (by default, this is "Source formatting") 42 | camm = "!f() { ticketId=$(git get-latest-jira-id $2); git commit -a -m \"$ticketId - ${1:-Source formatting}\"; }; f" 43 | cimm = "!f() { ticketId=$(git get-latest-jira-id $2); git commit -m \"$ticketId - ${1:-Source formatting}\"; }; f" 44 | 45 | ## Branches 46 | ##-------- 47 | 48 | # Checkout 49 | co = checkout 50 | # Checkout last branch 51 | col = checkout - 52 | # Branch 53 | br = branch 54 | # create a branch by name, or if it exists, checkout the branch 55 | cb = "!f() { git checkout -b $1 2> /dev/null && echo Created new branch $1 || `git checkout $1`; }; f" 56 | # "Refreshes" the current branch (deletes the local and remote, and recreates it) 57 | refresh = "!f() { git co $(git getm) && git db-all $1; git cb $1; }; f" 58 | # Move all commits after the passed one to a new branch 59 | # eg. git split 5c29ab4 custom-feature 60 | split = "!f() { git checkout -b $2 && msg=$(git stash save) && git checkout - && git reset --hard $1 && git checkout $2 && [[ ! $msg =~ ^'No local changes to save'$ ]] && git stash pop; }; f" 61 | 62 | ## Merging 63 | ##-------- 64 | 65 | # Merge 66 | mg = merge 67 | # Merge abort 68 | mga = merge --abort 69 | # Merge master into current branch 70 | mm = !git merge $(git getm) 71 | 72 | ## Rebasing 73 | ##-------- 74 | 75 | # Rebase 76 | rb = rebase 77 | # Abort rebase 78 | rba = rebase --abort 79 | # Continue rebase 80 | rbc = rebase --continue 81 | # Skip patch 82 | rbs = rebase --skip 83 | # Rebase interactively 84 | rbi = rebase -i 85 | # Rebase on top of master 86 | rbm = !git rebase $(git getm) 87 | # Rebase on top of master interactively 88 | rbim = rebase -i $(git getm) 89 | 90 | ## Resetting 91 | ##-------- 92 | 93 | # Reset 94 | rs = reset 95 | # Reset hard 96 | rsh = reset --hard 97 | # Reset soft 98 | rss = reset --soft 99 | 100 | ## Deleting 101 | ##-------- 102 | 103 | # Delete a local branch 104 | db = branch -D 105 | # Delete a remote branch 106 | db-remote = !sh -c 'git push origin :$0' 107 | # Delete both the local and remote branches 108 | db-all = !sh -c 'git db $0 && git db-remote $0' 109 | 110 | # Delete the current local branch and checkout master 111 | dbc = "!f() { local bn=$1; [[ -z $bn ]] && bn=$(git brn); master=$(git getm); [[ $bn == $master ]] && echo Cannot remove "$master" && exit; git co $master && git db $bn; }; f" 112 | 113 | ## Pushing 114 | ##-------- 115 | 116 | # Syncs from upstream, then pushes the master branch to origin and upstream 117 | push-allm = "!master=$(git getm); git sync-origin $master && git push origin $master && git push upstream $master;" 118 | # Syncs from upstream, then pushes the master branch to origin and upstream 119 | push-all = "!f() { current_branch=$(git brn); git sync-origin $current_branch && git push origin $current_branch && git push upstream $current_branch; }; f" 120 | 121 | pa = !git push-all 122 | pam = !git push-allm 123 | pum = push upstream $(git getm) 124 | pbo = !git push origin $(git brn) 125 | 126 | pu = pull upstream 127 | fu = fetch upstream 128 | 129 | ## Syncing 130 | ##-------- 131 | 132 | # Sync master from upstream to origin 133 | # This alias will stash current changes, switch to master, sync, then switch back to the original branch and apply the stash 134 | som = "!f() { local current_branch=$(git brn); local saved_index=0; local stash_save_result=`git stash save`; [[ $stash_save_result == *\"HEAD is now at\"* ]] && saved_index=1; master=$(git getm); git checkout $master && git sync-origin $master && git checkout $current_branch && ([[ $saved_index == 1 ]]) && git stash pop; }; f;" 135 | # Sync current branch with changes from upstream/master to the current branch on origin 136 | so = !git sync-origin $(git getm) 137 | # Sync current branch from upstream to origin 138 | sbo = !git sync-origin $(git brn) 139 | # Update current branch from origin 140 | ubo = !git pull origin $(git brn) 141 | 142 | ## Logging 143 | ##-------- 144 | 145 | # Pretty graph 146 | lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(cyan)<%an>%Creset' --abbrev-commit --date=relative 147 | lgd = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(cyan)<%an>%Creset' --abbrev-commit --date=default 148 | # lg graph with the names of the files that have changed 149 | lgst = !git lg --stat $@ 150 | # lg graph with the changes between the current branch and master 151 | lgm = !git lg $(git getm).. 152 | # lg graph with grepping on the log 153 | lgg = "!f() { git lg --grep=\"$1\"; }; f" 154 | # lg graph with searching the changes 155 | lgs = "!f() { git lg -S\"$1\"; }; f" 156 | # lg graph of just the latest commit 157 | hd = !git --no-pager log --pretty=format:'%Cred%h%Creset - %s %Cgreen(%cr) %C(cyan)<%an>%Creset' --abbrev-commit --date=relative -1 158 | head = !git log -n1 159 | tip = !git log --format=format:%h | head -1 160 | tipl = !git log --format=format:%H | head -1 161 | 162 | ## Stashing 163 | ##-------- 164 | 165 | # Apply stash (newest by default) 166 | sa = "!f() { local stash_rev=stash@{${1:-0}}; git stash apply $stash_rev; }; f" 167 | # Pop stash (newest by default) 168 | sp = "!f() { local stash_rev=stash@{${1:-0}}; git stash pop $stash_rev; }; f" 169 | # Save stash (includes untracked files) 170 | ss = !git add . && git stash save 171 | # List all entries in the stash 172 | sl = stash list 173 | 174 | ## Diffing 175 | ##-------- 176 | 177 | dt = difftool 178 | dtc = difftool --cached 179 | 180 | # List the filenames and diff stats for a commit/treeish 181 | dl = "!f() { ref_spec=$(git get-custom-refspec $1); git diff --stat $ref_spec; }; f" 182 | # Show the patch for a commit/treeish 183 | delta = "!f() { ref_spec=$(git get-custom-refspec $1); git diff $ref_spec; }; f" 184 | # Show *just* the file names that have been changed for a commit/treeish 185 | ldl = "!f() { local ref_spec=$(git get-custom-refspec $1); git diff --pretty='format:' --name-only $ref_spec; }; f" 186 | 187 | ## Opening files 188 | ##-------- 189 | 190 | # open batch of files 191 | bopen = "!f() { local ref_spec=$(git get-custom-refspec $1); editor=`git config --get user.editor`;files=`git diff --pretty=format: --name-only $ref_spec`;useopen=`command -v open`; usecygwin=`command -v cygstart`; [[ -n $useopen ]] && open -a \"$editor\" $files && exit $?; [[ -n $usecygwin ]] && editor=$(cygpath -d $editor); for i in $files; do echo opening $i; $usecygwin $editor $i; done }; f" 192 | # open each file individually 193 | open = "!f() { local ref_spec=$(git get-custom-refspec $1); editor=`git config --get user.editor`;files=`git diff --pretty=format: --name-only $ref_spec`;useopen=`command -v open`; usecygwin=`command -v cygstart`; [[ -n $useopen ]] && for i in $files; do echo opening $i; $useopen -a \"$editor\" $i; done && exit $?; [[ -n $usecygwin ]] && editor=$(cygpath -d $editor); for i in $files; do echo opening $i; $usecygwin $editor $i; done }; f" 194 | # Open all files that are different from master 195 | openm = !git open $(git getm).. 196 | 197 | ## Submodules 198 | ##-------- 199 | 200 | # Update submodules 201 | subu = submodule update 202 | # Update submodules and initialize them 203 | subi = submodule update --init 204 | 205 | ## Submitting Pull Requests 206 | ##-------- 207 | 208 | # Pull request 209 | pr = pull-request 210 | # Show pull requests w/stats 211 | prs = pull-request stat 212 | # Submit pull request and close the current one 213 | prcs = !git pr submit -q && git pr close 214 | # Sets the update branch based on the current repo directory name (so liferay-portal-ee-6.1.x will have an update branch of ee-6.1.x) 215 | set-pr-branch-config = "!f(){ git config "git-pull-request.$(git rev-parse --show-toplevel).$1" "$2"; }; f" 216 | set-update-branch = "!f(){ git set-pr-branch-config "update-branch" "${1:-$(git getm)}"; }; f" 217 | set-work-dir = "!f(){ git set-pr-branch-config "work-dir" "$1"; }; f" 218 | 219 | ## Misc 220 | ##-------- 221 | 222 | # Checkout master 223 | m = !git co $(git getm) 224 | 225 | # Shortcut for help 226 | h = help 227 | 228 | # Cherry-pick 229 | cp = cherry-pick 230 | 231 | # Get the current branch name 232 | brn = "!git branch $* | grep '^*' | sed 's/^* //'" 233 | 234 | # Get the last mentioned JIRA ticket id from the commit log 235 | get-latest-jira-id = "!f() { git log $1 --oneline | grep -Eo '([A-Z]{3,}-)([0-9]+)' -m 1; }; f" 236 | 237 | # Open the url to the latest JIRA ticket referenced in the log 238 | jira = "!f() { ticketId=$(git get-latest-jira-id $2); open http://issues.liferay.com/browse/$ticketId; }; f" 239 | 240 | # Get a range based commit-ish based on the passed sha. Uses HEAD by default, but if a sha is passed, it creates a range 241 | # that refers to one previous. If you pass a range, it will leave it untouched so that you can customize the range yourself. 242 | # Used in many aliases 243 | # eg. git get-custom-refspec # prints HEAD^..HEAD 244 | # git get-custom-refspec ^ # prints HEAD^.. 245 | # git get-custom-refspec 81c35e1 # prints 81c35e1^..81c35e1 246 | # git get-custom-refspec 5250022..master # prints 5250022..master 247 | get-custom-refspec = "!f() { to_rev=${1:-HEAD}; [[ $to_rev == ^ ]] && to_rev=HEAD^..; old_head=${to_rev}^; new_head=$to_rev; if [[ $to_rev == *..* ]]; then old_head=${to_rev%%..*}; new_head=${to_rev#*..*}; fi; ref_spec=$old_head..$new_head; echo $ref_spec; }; f" 248 | 249 | # Rank contributors by number of commits 250 | stats = shortlog -s -n 251 | 252 | # Same as add, but uses the verbose option since git add doesn't inform you of what exactly you just added 253 | aa = add -v 254 | 255 | # List ignored files 256 | ls-ignored = ls-files --exclude-standard --ignored --others 257 | 258 | # Reset last commit 259 | pop = reset HEAD^ 260 | 261 | # Remove untracked files and directories 262 | cln = clean -f -d -------------------------------------------------------------------------------- /git-pull-request/git-pull-request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Git command to automate many common tasks involving pull requests. 6 | 7 | Usage: 8 | 9 | gitpr [] [] 10 | 11 | Options: 12 | 13 | -h, --help 14 | Display this message. 15 | 16 | -r , --repo 17 | Use this github repo instead of the 'remote origin' or 'github.repo' 18 | git config setting. This can be either a remote name or a full 19 | repository name (user/repo). 20 | 21 | -u , --reviewer 22 | Send pull requests to this github repo instead of the 'remote upstream' 23 | or 'github.reviewer' git config setting. This can be either a username 24 | or a full repository name (user/repo). 25 | 26 | -b , --update-branch 27 | Specify the target branch on the reviewer github repository to submit the pull request. 28 | 29 | Commands: 30 | 31 | #no command# 32 | Displays a list of the open pull requests on this repository. 33 | 34 | #no command# 35 | Performs a fetch. 36 | 37 | alias 38 | Create an alias for the github name so you can use it in your git-pr submit 39 | command. 40 | 41 | close [] 42 | Closes the current pull request on github and deletes the pull request 43 | branch. 44 | 45 | continue-update, cu 46 | Continues the current update after conflicts have been fixed. 47 | 48 | fetch 49 | Fetches the pull request into a local branch, optionally updating it 50 | and checking it out. 51 | 52 | fetch-all 53 | Fetches all open pull requests into local branches. 54 | 55 | forward 56 | Forwards the specified pull request, set -u or --reviewer to specify a different reviewer. 57 | 58 | help 59 | Displays this message. 60 | 61 | info 62 | Displays a list of all the user's github repositories and the number 63 | of pull requests open on each. 64 | 65 | info-detailed 66 | Displays the same information as "info" but also lists the pull requests for each one (by user) 67 | 68 | merge 69 | Merges the current pull request branch into the update-branch and deletes the 70 | branch. 71 | 72 | open [] 73 | Opens either the current pull request or the specified request on 74 | github. 75 | 76 | pull 77 | Pulls remote changes from the other user's remote branch into the local 78 | pull request branch. 79 | 80 | show-alias 81 | Shows the github username pointed by the indicated alias. 82 | 83 | stats 84 | Fetches all open pull requests on this repository and displays them along 85 | with statistics about the pull requests and how many changes (along with how many 86 | changes by type). 87 | 88 | submit [] [] 89 | Pushes a branch and sends a pull request to the user's reviewer on 90 | github. 91 | 92 | update [] 93 | Updates the current pull request or the specified request with the local 94 | changes in the update-branch, using either a rebase or merge. 95 | 96 | update-users 97 | Updates the file configured in git-pull-request.users-alias-file variable. This file contains all the 98 | github names indexed by the email (without the @ email suffix). 99 | 100 | 101 | Copyright (C) 2011 Liferay, Inc. 102 | 103 | Based on scripts by: 104 | Connor McKay 105 | Andreas Gohr 106 | Minhchau Dang 107 | Nate Cavanaugh 108 | Miguel Pastor 109 | 110 | Released under the MIT License. 111 | """ 112 | 113 | import base64 114 | import codecs 115 | import getopt 116 | import getpass 117 | import io 118 | import json 119 | import os 120 | import re 121 | import sys 122 | import sys 123 | import tempfile 124 | import urllib 125 | import urllib3 126 | import webbrowser 127 | 128 | from string import Template 129 | from textwrap import fill 130 | 131 | UTF8Writer = codecs.getwriter("utf8") 132 | sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf-8') 133 | 134 | options = { 135 | "debug-mode": False, 136 | # Color Scheme 137 | "color-success": "green", 138 | "color-status": "blue", 139 | "color-error": "red", 140 | "color-warning": "red", 141 | "color-display-title-url": "cyan", 142 | "color-display-title-number": "magenta", 143 | "color-display-title-text": "red", 144 | "color-display-title-user": "blue", 145 | "color-display-info-repo-title": "default", 146 | "color-display-info-repo-count": "magenta", 147 | "color-display-info-total-title": "green", 148 | "color-display-info-total-count": "magenta", 149 | "color-stats-added": "yellow", 150 | "color-stats-average-change": "magenta", 151 | "color-stats-deleted": "red", 152 | "color-stats-total": "blue", 153 | # Disable the color scheme 154 | "enable-color": True, 155 | # Sets the default comment to post when closing a pull request. 156 | "close-default-comment": None, 157 | # Limit the number of characters from the description of the pull 158 | "description-char-limit": 500, 159 | # Set the indent character(s) used to indent the description 160 | "description-indent": " ", 161 | # Limit the number of lines from the description of the pull 162 | "description-line-limit": 1, 163 | # Set to true to remove the newlines from the description of the pull 164 | # (this will format it as it used to) 165 | "description-strip-newlines": False, 166 | # Determines whether fetch will automatically checkout the new branch. 167 | "fetch-auto-checkout": False, 168 | # Determines whether to automatically update a fetched pull request branch. 169 | # Setting this option to true will also cause the new branch to be checked 170 | # out. 171 | "fetch-auto-update": False, 172 | # Whether to show pull requests for the entire repo or just the update-branch. 173 | "filter-by-update-branch": True, 174 | # Determines whether to automatically close pull requests after merging 175 | # them. 176 | "merge-auto-close": True, 177 | # A string to be used to append to the end of each result of the stats command. 178 | # It's passed the merge_base SHA, the branch name of the fetched pull, as well as 179 | # a list of the committers that contributed to the pull. 180 | # Example: 'Diff: ${merge_base}..${branch_name}, ${NEWLINE}Committers: ${committers}' 181 | # 182 | # If the string starts with the ` character, then the script will treat the rest 183 | # of the string as a shell script. However, the merge_base, branch_name and committers 184 | # are still substituted in, allowing you to send them to another script in any order 185 | # Example: '`git log --oneline ${merge_base}..${branch_name} && echo "${committers}"' 186 | "stats-footer": None, 187 | # A string to be used to format the message sent with a submitted pull. 188 | # Available variables: 189 | # ${merge_base}: SHA of the merge base of this branch 190 | # ${branch_name}: the branch name of the current pull 191 | # ${committers}: committers on this pull request 192 | # ${pull_body}: the pull body passed via the command line 193 | # ${reviewer}: the username of the reviewer 194 | # ${repo_name}: the name of the repo you're submitting from/to 195 | # Example: 'Hi ${reviewer}, I'm sending the commits ${merge_base}..${branch_name}. kthxbye!' 196 | # 197 | # If the string starts with the ` character, then the script will treat the rest 198 | # of the string as a shell script. However, the variables are still substituted in, 199 | # allowing you to send them to another script in any order 200 | # Example: '`echo "${pull_body}" | tr "[:upper:]" "[:lower:]"' 201 | "format-submit-body": None, 202 | # Sets the branch to use where updates are merged from or to. 203 | "update-branch": "master", 204 | # Sets the method to use when updating pull request branches with changes 205 | # in the update-branch. 206 | # Possible options: 'merge', 'rebase' 207 | "update-method": "merge", 208 | # The organization to update users from (set to None or an empty string to update from the current fork) 209 | "user-organization": "liferay", 210 | # Determines whether to open newly submitted pull requests on github 211 | "submit-open-github": True, 212 | # Sets a directory to be used for performing updates to prevent 213 | # excessive rebuilding by IDE's. Warning: This directory will be hard reset 214 | # every time an update is performed, so do not do any work other than 215 | # conflict merges in the work directory. 216 | "work-dir": None, 217 | } 218 | 219 | URL_BASE = "https://api.github.com/%s" 220 | SCRIPT_NOTE = "GitPullRequest Script (by Liferay)" 221 | TMP_PATH = tempfile.gettempdir() + "/%s" 222 | 223 | MAP_RESPONSE = {} 224 | 225 | def build_branch_name(pull_request): 226 | """Returns the local branch name that a pull request should be fetched into""" 227 | ref = pull_request["head"]["ref"] 228 | 229 | request_id = pull_request["number"] 230 | 231 | branch_name = "pull-request-%s" % request_id 232 | 233 | jira_ticket = get_jira_ticket(ref) 234 | 235 | if not jira_ticket: 236 | jira_ticket = get_jira_ticket(pull_request["title"]) 237 | 238 | if jira_ticket: 239 | branch_name = "%s-%s" % (branch_name, jira_ticket) 240 | 241 | return branch_name 242 | 243 | 244 | def build_pull_request_title(branch_name): 245 | """Returns the default title to use for a pull request for the branch with 246 | the name""" 247 | 248 | jira_ticket = get_jira_ticket(branch_name) 249 | 250 | if jira_ticket: 251 | branch_name = jira_ticket 252 | 253 | return branch_name 254 | 255 | 256 | def chdir(dir): 257 | f = open(get_tmp_path("git-pull-request-chdir"), "wb") 258 | f.write(dir) 259 | f.close() 260 | 261 | 262 | def close_pull_request(repo_name, pull_request_ID, comment=None): 263 | default_comment = options["close-default-comment"] 264 | 265 | if comment is None: 266 | comment = default_comment 267 | 268 | if comment is None or comment == default_comment: 269 | try: 270 | f = open(get_tmp_path("git-pull-request-treeish-%s" % pull_request_ID), "r") 271 | branch_info = json.load(f) 272 | f.close() 273 | 274 | username = branch_info["username"] 275 | 276 | updated_parent_commit = "" 277 | updated_head_commit = "" 278 | original_parent_commit = "" 279 | original_head_commit = "" 280 | 281 | if "original" in branch_info: 282 | original = branch_info["original"] 283 | original_parent_commit = original["parent_commit"] 284 | original_head_commit = original["head_commit"] 285 | 286 | if "updated" in branch_info: 287 | updated = branch_info["updated"] 288 | updated_parent_commit = updated["parent_commit"] 289 | updated_head_commit = updated["head_commit"] 290 | 291 | current_head_commit = os.popen("git rev-parse HEAD").read().strip()[0:10] 292 | 293 | my_diff_comment = "" 294 | 295 | diff_commit = False 296 | 297 | if original_head_commit != current_head_commit: 298 | current_diff_tree = ( 299 | os.popen("git diff-tree -r -c -M -C --no-commit-id HEAD") 300 | .read() 301 | .strip() 302 | ) 303 | original_diff_tree = ( 304 | os.popen( 305 | "git diff-tree -r -c -M -C --no-commit-id %s" 306 | % original_head_commit 307 | ) 308 | .read() 309 | .strip() 310 | ) 311 | 312 | current_tree_commits = current_diff_tree.split("\n") 313 | original_tree_commits = original_diff_tree.split("\n") 314 | 315 | if len(current_tree_commits) == len(original_tree_commits): 316 | for index, commit in enumerate(current_tree_commits): 317 | current_commits = commit.split(" ") 318 | original_commits = original_tree_commits[index].split(" ") 319 | 320 | if ( 321 | len(current_commits) >= 4 322 | and len(original_commits) >= 4 323 | and current_commits[3] != original_commits[3] 324 | ): 325 | diff_commit = True 326 | break 327 | else: 328 | diff_commit = True 329 | 330 | if (updated_head_commit or original_head_commit) == current_head_commit: 331 | diff_commit = False 332 | 333 | if diff_commit: 334 | my_diff_comment = ( 335 | "\n\nView just my changes: https://github.com/%s/compare/%s:%s...%s" 336 | % ( 337 | repo_name, 338 | username, 339 | updated_head_commit or original_head_commit, 340 | current_head_commit, 341 | ) 342 | ) 343 | 344 | if comment is None: 345 | comment = "" 346 | 347 | new_pr_url = meta("new_pr_url") 348 | 349 | if new_pr_url and new_pr_url != "": 350 | comment += "\nPull request submitted at: %s" % new_pr_url 351 | 352 | comment += my_diff_comment 353 | 354 | comment += "\nView total diff: https://github.com/%s/compare/%s...%s" % ( 355 | repo_name, 356 | (updated_parent_commit or original_parent_commit), 357 | current_head_commit, 358 | ) 359 | except Exception: 360 | pass 361 | 362 | if comment is not None and comment != "": 363 | post_comment(repo_name, pull_request_ID, comment) 364 | 365 | url = get_api_url("repos/%s/pulls/%s" % (repo_name, pull_request_ID)) 366 | 367 | params = {"state": "closed"} 368 | 369 | github_json_request(url, params) 370 | 371 | 372 | def color_text(text, token, bold=False): 373 | """Return the given text in ANSI colors""" 374 | 375 | # http://travelingfrontiers.wordpress.com/2010/08/22/how-to-add-colors-to-linux-command-line-output/ 376 | 377 | if options["enable-color"] == True: 378 | color_name = options["color-%s" % token] 379 | 380 | if color_name == "default" or (not FORCE_COLOR and not sys.stdout.isatty()): 381 | return text 382 | 383 | colors = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white") 384 | 385 | if color_name in colors: 386 | return u"\033[{0};{1}m{2}\033[0m".format( 387 | int(bold), colors.index(color_name) + 30, text 388 | ) 389 | else: 390 | return text 391 | else: 392 | return text 393 | 394 | 395 | def command_alias(alias, githubname, filename): 396 | try: 397 | users[alias] = githubname 398 | except Exception: 399 | raise UserWarning("Error while updating the alias for %s" % alias) 400 | 401 | github_users_file = open(filename, "w") 402 | json.dump(users, github_users_file) 403 | 404 | github_users_file.close() 405 | 406 | 407 | def command_close(repo_name, comment=None): 408 | """Closes the current pull request on github with the optional comment, then 409 | deletes the branch.""" 410 | 411 | print(color_text("Closing pull request", "status")) 412 | print 413 | 414 | branch_name = get_current_branch_name() 415 | pull_request_ID = get_pull_request_ID(branch_name) 416 | pull_request = get_pull_request(repo_name, pull_request_ID) 417 | 418 | display_pull_request(pull_request) 419 | 420 | close_pull_request(repo_name, pull_request_ID, comment) 421 | 422 | update_branch_option = options["update-branch"] 423 | 424 | ret = os.system("git checkout %s" % update_branch_option) 425 | if ret != 0: 426 | raise UserWarning("Could not checkout %s" % update_branch_option) 427 | 428 | print(color_text("Deleting branch %s" % branch_name, "status")) 429 | ret = os.system("git branch -D %s" % branch_name) 430 | if ret != 0: 431 | raise UserWarning("Could not delete branch") 432 | 433 | print 434 | print(color_text("Pull request closed", "success")) 435 | print 436 | display_status() 437 | 438 | 439 | def command_comment(repo_name, comment=None, pull_request_ID=None): 440 | if pull_request_ID is None: 441 | branch_name = get_current_branch_name() 442 | pull_request_ID = get_pull_request_ID(branch_name) 443 | 444 | if comment is not None and comment != "": 445 | post_comment(repo_name, pull_request_ID, comment) 446 | else: 447 | raise UserWarning("Please include a comment") 448 | 449 | 450 | def command_continue_update(): 451 | print(color_text("Continuing update from %s" % options["update-branch"], "status")) 452 | 453 | continue_update() 454 | print 455 | display_status() 456 | 457 | 458 | def command_fetch(repo_name, pull_request_ID, auto_update=False): 459 | """Fetches a pull request into a local branch""" 460 | 461 | print(color_text("Fetching pull request", "status")) 462 | print 463 | 464 | pull_request = get_pull_request(repo_name, pull_request_ID) 465 | display_pull_request(pull_request) 466 | branch_name = fetch_pull_request(pull_request, repo_name) 467 | 468 | parent_commit = pull_request["base"]["sha"] 469 | head_commit = pull_request["head"]["sha"] 470 | username = pull_request["user"]["login"] 471 | 472 | branch_info = { 473 | "username": username, 474 | "original": { 475 | "parent_commit": parent_commit[0:10], 476 | "head_commit": head_commit[0:10], 477 | }, 478 | } 479 | 480 | f = open(get_tmp_path("git-pull-request-treeish-%s" % pull_request_ID), "w") 481 | branch_treeish = json.dump(branch_info, f) 482 | f.close() 483 | 484 | if auto_update: 485 | update_branch(branch_name) 486 | elif options["fetch-auto-checkout"]: 487 | ret = os.system("git checkout %s" % branch_name) 488 | if ret != 0: 489 | raise UserWarning("Could not checkout %s" % branch_name) 490 | 491 | print 492 | print(color_text("Fetch completed", "success")) 493 | print 494 | display_status() 495 | 496 | return pull_request 497 | 498 | 499 | def command_fetch_all(repo_name): 500 | """Fetches all pull requests into local branches""" 501 | 502 | print(color_text("Fetching all pull requests", "status")) 503 | print 504 | 505 | pull_requests = get_pull_requests(repo_name, options["filter-by-update-branch"]) 506 | 507 | for pull_request in pull_requests: 508 | fetch_pull_request(pull_request, repo_name) 509 | display_pull_request_minimal(pull_request) 510 | print 511 | 512 | display_status() 513 | 514 | 515 | def command_forward(repo_name, pull_request_ID, username, reviewer_repo_name): 516 | branch_name = get_current_branch_name(False) 517 | 518 | if branch_name.find('-%s-' % pull_request_ID) == -1: 519 | auto_checkout = options['fetch-auto-checkout'] 520 | 521 | options['fetch-auto-checkout'] = True 522 | old_pull_request = command_fetch(repo_name, pull_request_ID, True) 523 | options['fetch-auto-checkout'] = auto_checkout 524 | else: 525 | old_pull_request = get_pull_request(repo_name, pull_request_ID) 526 | 527 | quoted_body = '' if old_pull_request['body'] is None else '> ' + '\n> '.join(old_pull_request['body'].split('\n')) 528 | 529 | forwarded_body = '/cc @%s\n\nForwarded from %s\n\n%s' % (old_pull_request['user']['login'], old_pull_request['html_url'], quoted_body) 530 | 531 | update_branch_name = options['update-branch'] 532 | 533 | options['update-branch'] = old_pull_request['base']['ref'] 534 | 535 | new_pull_request = command_submit( 536 | repo_name, 537 | username, 538 | reviewer_repo_name=reviewer_repo_name, 539 | pull_body=forwarded_body, 540 | pull_title=old_pull_request['title'], 541 | submitOpenGitHub=False 542 | ) 543 | 544 | command_close(repo_name, 'Forwarded to %s' % new_pull_request['html_url']) 545 | 546 | options['update-branch'] = update_branch_name 547 | 548 | def command_help(): 549 | print(__doc__) 550 | 551 | 552 | def command_info(username, detailed=False): 553 | print(color_text("Loading information on repositories for %s" % username, "status")) 554 | print 555 | 556 | # Change URL depending on if info user is passed in 557 | 558 | if username == DEFAULT_USERNAME: 559 | url = "user/repos" 560 | else: 561 | url = "users/%s/repos" % username 562 | 563 | url = get_api_url(url) 564 | 565 | url += "?per_page=100&type=owner" 566 | 567 | repos = github_json_request(url) 568 | 569 | total = 0 570 | 571 | current_base_name = "" 572 | 573 | for pull_request_info in repos: 574 | issue_count = pull_request_info["open_issues"] 575 | 576 | if issue_count > 0: 577 | base_name = pull_request_info["name"] 578 | 579 | if base_name != current_base_name: 580 | current_base_name = base_name 581 | print("") 582 | print("%s:" % color_text(base_name, "display-title-text")) 583 | print("---------") 584 | 585 | repo_name = "%s/%s" % (pull_request_info["owner"]["login"], base_name) 586 | 587 | print( 588 | " %s: %s" % ( 589 | color_text(base_name, "display-info-repo-title"), 590 | color_text(issue_count, "display-info-repo-count"), 591 | )) 592 | 593 | if detailed: 594 | pull_requests = get_pull_requests(repo_name, False) 595 | 596 | current_branch_name = "" 597 | 598 | for pull_request in pull_requests: 599 | branch_name = pull_request["base"]["ref"] 600 | if branch_name != current_branch_name: 601 | current_branch_name = branch_name 602 | print("") 603 | print(" %s:" % color_text( 604 | current_branch_name, "display-title-user" 605 | )) 606 | 607 | print(" %s" % display_pull_request_minimal( 608 | pull_request, True 609 | )) 610 | 611 | total += issue_count 612 | 613 | print("-") 614 | out = "%s: %s" % ( 615 | color_text("Total pull requests", "display-info-total-title", True), 616 | color_text(total, "display-info-total-count", True), 617 | ) 618 | print 619 | display_status() 620 | return out 621 | 622 | 623 | def command_merge(repo_name, comment=None): 624 | """Merges changes from the local pull request branch into the update-branch and deletes 625 | the pull request branch""" 626 | 627 | branch_name = get_current_branch_name() 628 | pull_request_ID = get_pull_request_ID(branch_name) 629 | 630 | update_branch_option = options["update-branch"] 631 | 632 | print(color_text( 633 | "Merging %s into %s" % (branch_name, update_branch_option), "status" 634 | )) 635 | print 636 | 637 | ret = os.system("git checkout %s" % update_branch_option) 638 | if ret != 0: 639 | raise UserWarning("Could not checkout %s" % update_branch_option) 640 | 641 | ret = os.system("git merge %s" % branch_name) 642 | if ret != 0: 643 | raise UserWarning( 644 | "Merge with %s failed. Resolve conflicts, switch back into the pull request branch, and merge again" 645 | % update_branch_option 646 | ) 647 | 648 | print(color_text("Deleting branch %s" % branch_name, "status")) 649 | ret = os.system("git branch -D %s" % branch_name) 650 | if ret != 0: 651 | raise UserWarning("Could not delete branch") 652 | 653 | if options["merge-auto-close"]: 654 | print(color_text("Closing pull request", "status")) 655 | close_pull_request(repo_name, pull_request_ID, comment) 656 | 657 | print 658 | print(color_text("Merge completed", "success")) 659 | print 660 | display_status() 661 | 662 | 663 | def command_open(repo_name, pull_request_ID=None): 664 | """Open a pull request in the browser""" 665 | 666 | if pull_request_ID is None: 667 | branch_name = get_current_branch_name() 668 | pull_request_ID = get_pull_request_ID(branch_name) 669 | 670 | pull_request = get_pull_request(repo_name, pull_request_ID) 671 | 672 | open_URL(pull_request.get("html_url")) 673 | 674 | 675 | def command_pull(repo_name): 676 | """Pulls changes from the remote branch into the local branch of the pull 677 | request""" 678 | 679 | branch_name = get_current_branch_name() 680 | 681 | print(color_text("Pulling remote changes into %s" % branch_name, "status")) 682 | 683 | pull_request_ID = get_pull_request_ID(branch_name) 684 | 685 | pull_request = get_pull_request(repo_name, pull_request_ID) 686 | repo_url = get_repo_url(pull_request, repo_name) 687 | 688 | branch_name = build_branch_name(pull_request) 689 | remote_branch_name = "refs/pull/%s/head" % pull_request["number"] 690 | 691 | print(color_text( 692 | "Pulling from %s (%s)" % (repo_url, pull_request["head"]["ref"]), "status" 693 | )) 694 | 695 | ret = os.system("git pull %s %s" % (repo_url, remote_branch_name)) 696 | 697 | if ret != 0: 698 | raise UserWarning("Pull failed, resolve conflicts") 699 | 700 | print 701 | print(color_text("Updating %s from remote completed" % branch_name, "success")) 702 | print 703 | display_status() 704 | 705 | 706 | def command_show(repo_name): 707 | """List open pull requests 708 | 709 | Queries the github API for open pull requests in the current repo. 710 | """ 711 | 712 | update_branch_name = options["update-branch"] 713 | filter_by_update_branch = options["filter-by-update-branch"] 714 | 715 | if not filter_by_update_branch: 716 | update_branch_name = "across all branches" 717 | else: 718 | update_branch_name = "on branch '%s'" % update_branch_name 719 | 720 | print(color_text( 721 | "Loading open pull requests for %s %s" % (repo_name, update_branch_name), 722 | "status", 723 | )) 724 | print 725 | 726 | pull_requests = get_pull_requests(repo_name, filter_by_update_branch) 727 | 728 | if len(pull_requests) == 0: 729 | print("No open pull requests found") 730 | 731 | for pull_request in pull_requests: 732 | display_pull_request(pull_request) 733 | 734 | display_status() 735 | 736 | 737 | def command_show_alias(alias): 738 | """Shows the username where the alias points to""" 739 | 740 | user_item = next( 741 | (user for user in users.items() if user[0] == alias or user[1] == alias), 742 | None, 743 | ) 744 | 745 | if user_item: 746 | print("The user alias %s points to %s " % user_item) 747 | else: 748 | print("There is no user alias or github name matching %s in the current mapping file" % alias) 749 | 750 | 751 | def command_submit( 752 | repo_name, 753 | username, 754 | reviewer_repo_name=None, 755 | pull_body=None, 756 | pull_title=None, 757 | submitOpenGitHub=True, 758 | ): 759 | """Push the current branch and create a pull request to your github reviewer 760 | (or upstream)""" 761 | 762 | branch_name = get_current_branch_name(False) 763 | 764 | print(color_text("Submitting pull request for %s" % branch_name, "status")) 765 | 766 | if reviewer_repo_name is None or reviewer_repo_name == "": 767 | reviewer_repo_name = get_repo_name_for_remote("upstream") 768 | 769 | if reviewer_repo_name is None or reviewer_repo_name == "": 770 | raise UserWarning("Could not determine a repo to submit this pull request to") 771 | 772 | if "/" not in reviewer_repo_name: 773 | reviewer_repo_name = repo_name.replace(username, reviewer_repo_name) 774 | 775 | print(color_text("Pushing local branch %s to origin" % branch_name, "status")) 776 | 777 | ret = os.system("git push origin %s" % branch_name) 778 | 779 | if ret != 0: 780 | raise UserWarning("Could not push this branch to your origin") 781 | 782 | url = get_api_url("repos/%s/pulls" % reviewer_repo_name) 783 | 784 | if pull_title == None or pull_title == "": 785 | pull_title = build_pull_request_title(branch_name) 786 | 787 | format_submit_body = options["format-submit-body"] 788 | 789 | if format_submit_body: 790 | merge_base = ( 791 | os.popen("git merge-base %s %s" % (options["update-branch"], branch_name)) 792 | .read() 793 | .strip() 794 | ) 795 | committers = ( 796 | os.popen( 797 | "git log {0}..{1} --pretty='%an' --reverse | awk ' !x[$0]++'".format( 798 | merge_base, branch_name 799 | ) 800 | ) 801 | .read() 802 | .strip() 803 | ) 804 | committers = committers.split(os.linesep) 805 | committers = ", ".join(committers) 806 | 807 | fn = False 808 | 809 | if format_submit_body.startswith("`"): 810 | format_submit_body = format_submit_body[1:] 811 | fn = True 812 | 813 | pull_body_tpl = Template(format_submit_body) 814 | 815 | committers = committers.decode("utf-8") 816 | 817 | reviewer_repo_pieces = reviewer_repo_name.split("/") 818 | reviewer = reviewer_repo_pieces[0] 819 | 820 | variables = { 821 | "merge_base": merge_base[0:8], 822 | "branch_name": branch_name, 823 | "committers": committers, 824 | "pull_body": pull_body or "", 825 | "reviewer": reviewer, 826 | "repo_name": reviewer_repo_pieces[1], 827 | "NEWLINE": os.linesep, 828 | } 829 | 830 | pull_body_result = pull_body_tpl.safe_substitute(**variables) 831 | 832 | if fn: 833 | pull_body_result = ( 834 | os.popen(pull_body_result.encode("utf-8")) 835 | .read() 836 | .strip() 837 | .decode("utf-8") 838 | ) 839 | 840 | if pull_body_result: 841 | pull_body = pull_body_result 842 | 843 | if pull_body == None: 844 | pull_body = "" 845 | 846 | params = { 847 | "base": options["update-branch"], 848 | "head": "%s:%s" % (username, branch_name), 849 | "title": pull_title, 850 | "body": pull_body, 851 | } 852 | 853 | print(color_text("Sending pull request to %s" % reviewer_repo_name, "status")) 854 | 855 | pull_request = None 856 | 857 | try: 858 | pull_request = github_json_request(url, params) 859 | except Exception as e: 860 | msg = e 861 | 862 | if not pull_request: 863 | print("Couldn't get a response from github, going to check if the pull was submitted anyways...") 864 | 865 | reviewer_pulls = get_pull_requests(reviewer_repo_name, True) 866 | 867 | for pr in reviewer_pulls: 868 | if ( 869 | pr.get("user").get("login") == DEFAULT_USERNAME 870 | and pr.get("head").get("ref") == branch_name 871 | ): 872 | pull_request = pr 873 | 874 | if not pull_request: 875 | raise msg 876 | 877 | new_pr_url = pull_request.get("html_url") 878 | 879 | if new_pr_url and new_pr_url != "": 880 | meta("new_pr_url", new_pr_url) 881 | 882 | print 883 | display_pull_request(pull_request) 884 | print 885 | 886 | print(color_text("Pull request submitted", "success")) 887 | print 888 | display_status() 889 | 890 | if submitOpenGitHub: 891 | open_URL(new_pr_url) 892 | 893 | return pull_request 894 | 895 | 896 | def command_update(repo_name, target=None): 897 | if target == None: 898 | branch_name = get_current_branch_name() 899 | else: 900 | try: 901 | pull_request_ID = int(target) 902 | pull_request = get_pull_request(repo_name, pull_request_ID) 903 | branch_name = build_branch_name(pull_request) 904 | except ValueError: 905 | branch_name = target 906 | 907 | print(color_text( 908 | "Updating %s from %s" % (branch_name, options["update-branch"]), "status" 909 | )) 910 | 911 | update_branch(branch_name) 912 | print 913 | display_status() 914 | 915 | 916 | def command_update_meta(): 917 | update_meta() 918 | 919 | 920 | def command_update_users( 921 | filename, url=None, github_users=None, total_pages=0, all_pages=True 922 | ): 923 | if url is None: 924 | user_organization = options["user-organization"] 925 | 926 | if user_organization: 927 | url = get_api_url("orgs/%s/members" % user_organization) 928 | else: 929 | url = get_api_url("repos/%s/forks" % get_repo_name_for_remote("upstream")) 930 | 931 | params = {"per_page": "100", "sort": "oldest"} 932 | 933 | url_parts = list(urllib.parse.urlparse(url)) 934 | query = dict(urllib.parse.parse_qsl(url_parts[4])) 935 | query.update(params) 936 | 937 | url_parts[4] = urllib.parse.urlencode(query) 938 | 939 | url = urllib.parse.urlunparse(url_parts) 940 | 941 | if github_users is None: 942 | github_users = {} 943 | 944 | items = github_json_request(url) 945 | 946 | m = re.search(r"[?&]page=(\d+)", url) 947 | 948 | if m is not None and m.group(1) != "": 949 | print("Doing another request for page: %s of %s" % (m.group(1), total_pages)) 950 | else: 951 | print("There are more than %s users, this could take a few minutes..." % len( 952 | items 953 | )) 954 | 955 | user_api_url = get_api_url("users") 956 | 957 | for item in items: 958 | user_info = item 959 | 960 | if "owner" in item: 961 | user_info = item["owner"] 962 | 963 | login = user_info["login"] 964 | 965 | github_user_info = github_json_request("%s/%s" % (user_api_url, login)) 966 | email = login 967 | 968 | email = get_user_email(github_user_info) 969 | 970 | if email != None: 971 | github_users[email] = login 972 | 973 | if all_pages: 974 | link_header = MAP_RESPONSE[url].headers.get("Link") 975 | 976 | if link_header is not None: 977 | m = re.search(r'<([^>]+)>; rel="next",', link_header) 978 | 979 | if m is not None and m.group(1) != "": 980 | url = m.group(1) 981 | 982 | if total_pages == 0: 983 | m1 = re.search( 984 | r'<[^>]+[&?]page=(\d+)[^>]*>; rel="last"', link_header 985 | ) 986 | 987 | if m1 is not None and m1.group(1) != "": 988 | total_pages = m1.group(1) 989 | 990 | command_update_users(filename, url, github_users, total_pages) 991 | 992 | github_users_file = open(filename, "w") 993 | json.dump(github_users, github_users_file) 994 | 995 | github_users_file.close() 996 | 997 | return github_users 998 | 999 | 1000 | def complete_update(branch_name): 1001 | update_branch_option = options["update-branch"] 1002 | 1003 | if in_work_dir(): 1004 | ret = os.system("git checkout %s" % update_branch_option) 1005 | if ret != 0: 1006 | raise UserWarning( 1007 | "Could not checkout %s branch in work directory" % update_branch_option 1008 | ) 1009 | 1010 | original_dir_path = get_original_dir_path() 1011 | 1012 | print(color_text( 1013 | "Switching to original directory: '%s'" % original_dir_path, "status" 1014 | )) 1015 | 1016 | os.chdir(original_dir_path) 1017 | chdir(original_dir_path) 1018 | 1019 | if get_current_branch_name(False) == branch_name: 1020 | ret = os.system("git reset --hard && git clean -f") 1021 | if ret != 0: 1022 | raise UserWarning( 1023 | "Syncing branch %s with work directory failed" % branch_name 1024 | ) 1025 | else: 1026 | ret = os.system("git checkout %s" % branch_name) 1027 | if ret != 0: 1028 | raise UserWarning("Could not checkout %s" % branch_name) 1029 | 1030 | update_branch_option = options["update-branch"] 1031 | 1032 | branch_treeish = update_meta() 1033 | 1034 | print 1035 | print(color_text( 1036 | "Updating %s from %s complete" % (branch_name, update_branch_option), "success" 1037 | )) 1038 | 1039 | 1040 | def continue_update(): 1041 | if options["update-method"] == "merge": 1042 | ret = os.system("git commit") 1043 | elif options["update-method"] == "rebase": 1044 | ret = os.system("git rebase --continue") 1045 | 1046 | if ret != 0: 1047 | raise UserWarning( 1048 | "Updating from %s failed\nResolve conflicts and 'git add' files, then run 'gitpr continue-update'" 1049 | % options["update-branch"] 1050 | ) 1051 | 1052 | # The branch name will not be correct until the merge/rebase is complete 1053 | branch_name = get_current_branch_name() 1054 | 1055 | complete_update(branch_name) 1056 | 1057 | 1058 | def display_pull_request(pull_request): 1059 | """Nicely display_pull_request info about a given pull request""" 1060 | 1061 | display_pull_request_minimal(pull_request) 1062 | 1063 | description_indent = options["description-indent"] 1064 | 1065 | print("%s%s" % ( 1066 | description_indent, 1067 | color_text(pull_request.get("html_url"), "display-title-url"), 1068 | )) 1069 | 1070 | pr_body = pull_request.get("body") 1071 | 1072 | if pr_body and pr_body.strip(): 1073 | pr_body = strip_html_tags(pr_body) 1074 | 1075 | pr_body = re.sub(r"()", "\n", pr_body.strip()) 1076 | 1077 | if options["description-strip-newlines"]: 1078 | pr_body = fill( 1079 | pr_body, 1080 | initial_indent=description_indent, 1081 | subsequent_indent=description_indent, 1082 | width=80, 1083 | ) 1084 | else: 1085 | # Normalize newlines 1086 | pr_body = re.sub("\r?\n", "\n", pr_body) 1087 | 1088 | pr_body = pr_body.splitlines() 1089 | 1090 | pr_body = [ 1091 | fill( 1092 | line.strip(), 1093 | initial_indent=description_indent, 1094 | subsequent_indent=description_indent, 1095 | width=80, 1096 | ) 1097 | for line in pr_body 1098 | ] 1099 | 1100 | pr_body = "\n".join(pr_body) 1101 | 1102 | pr_body = strip_empty_lines(pr_body) 1103 | 1104 | if ((options["description-line-limit"] >= 0) and (len(pr_body.splitlines()) > options["description-line-limit"])): 1105 | pr_body = pr_body.splitlines() 1106 | pr_body = pr_body[:options["description-line-limit"]] 1107 | pr_body = "\n".join(pr_body) 1108 | #pr_body += "..." 1109 | 1110 | if ((options["description-char-limit"] >= 0) and (len(pr_body) > options["description-char-limit"])): 1111 | pr_body = pr_body[:options["description-char-limit"]] # + '...' 1112 | 1113 | print(pr_body) 1114 | 1115 | print 1116 | 1117 | 1118 | def display_pull_request_minimal(pull_request, return_text=False): 1119 | """Display minimal info about a given pull request""" 1120 | 1121 | text = "%s - %s (%s)" % ( 1122 | color_text( 1123 | "REQUEST %s" % pull_request.get("number"), "display-title-number", True 1124 | ), 1125 | color_text(pull_request.get("title"), "display-title-text", True), 1126 | color_text(pull_request["user"].get("login"), "display-title-user"), 1127 | ) 1128 | 1129 | if return_text: 1130 | return text 1131 | 1132 | print(text) 1133 | 1134 | 1135 | def display_status(): 1136 | """Displays the current branch name""" 1137 | 1138 | branch_name = get_current_branch_name(False) 1139 | out = "Current branch: %s" % branch_name 1140 | print(out) 1141 | return out 1142 | 1143 | 1144 | def fetch_pull_request(pull_request, repo_name): 1145 | """Fetches a pull request into a local branch, and returns the name of the 1146 | local branch""" 1147 | 1148 | branch_name = build_branch_name(pull_request) 1149 | repo_url = get_repo_url(pull_request, repo_name) 1150 | 1151 | remote_branch_name = "refs/pull/%s/head" % pull_request["number"] 1152 | 1153 | ret = os.system("git show-ref --verify -q refs/heads/%s" % branch_name) 1154 | # sha = os.popen('git rev-parse --abbrev-ref refs/heads/%s' % branch_name).read().strip() 1155 | 1156 | # log(pull_request) 1157 | 1158 | if ret != 0: 1159 | ret = os.system( 1160 | 'git fetch %s "%s":%s' % (repo_url, remote_branch_name, branch_name) 1161 | ) 1162 | 1163 | if ret != 0: 1164 | ret = os.system("git show-ref --verify refs/heads/%s" % branch_name) 1165 | 1166 | if ret != 0: 1167 | print("Could not get from refs/pull/%s/head, trying to brute force the fetch" % pull_request[ 1168 | "number" 1169 | ]) 1170 | 1171 | repo_url = get_repo_url(pull_request, repo_name, True) 1172 | remote_branch_name = pull_request["head"]["ref"] 1173 | 1174 | ret = os.system( 1175 | 'git fetch %s "%s":%s' % (repo_url, remote_branch_name, branch_name) 1176 | ) 1177 | 1178 | if ret != 0: 1179 | ret = os.system("git show-ref --verify refs/heads/%s" % branch_name) 1180 | 1181 | if ret != 0: 1182 | raise UserWarning("Fetch failed") 1183 | 1184 | try: 1185 | os.remove(get_tmp_path("git-pull-request-treeish-%s" % pull_request["number"])) 1186 | except OSError: 1187 | pass 1188 | 1189 | return branch_name 1190 | 1191 | 1192 | def get_api_url(command): 1193 | return URL_BASE % command 1194 | 1195 | 1196 | def get_current_branch_name(ensure_pull_request=True): 1197 | """Returns the name of the current pull request branch""" 1198 | branch_name = os.popen("git rev-parse --abbrev-ref HEAD").read().strip() 1199 | 1200 | if ensure_pull_request and branch_name[0:13] != "pull-request-": 1201 | raise UserWarning("Invalid branch: not a pull request") 1202 | 1203 | return branch_name 1204 | 1205 | 1206 | def get_default_repo_name(): 1207 | repo_name = os.popen("git config github.repo").read().strip() 1208 | 1209 | # get repo name from origin 1210 | if repo_name is None or repo_name == "": 1211 | repo_name = get_repo_name_for_remote("origin") 1212 | 1213 | if repo_name is None or repo_name == "": 1214 | raise UserWarning("Failed to determine github repository name") 1215 | 1216 | return repo_name 1217 | 1218 | 1219 | def get_git_base_path(): 1220 | return os.popen("git rev-parse --show-toplevel").read().strip() 1221 | 1222 | 1223 | def get_jira_ticket(text): 1224 | """Returns a JIRA ticket id from the passed text, or a blank string otherwise""" 1225 | m = re.search(r"[A-Z]{3,}-\d+", text) 1226 | 1227 | jira_ticket = "" 1228 | 1229 | if m != None and m.group(0) != "": 1230 | jira_ticket = m.group(0) 1231 | 1232 | return jira_ticket 1233 | 1234 | 1235 | def get_original_dir_path(): 1236 | git_base_path = get_git_base_path() 1237 | 1238 | f = open(os.path.join(get_work_dir(), ".git", "original_dir_path"), "rb") 1239 | original_dir_path = f.read() 1240 | f.close() 1241 | 1242 | if original_dir_path == None or original_dir_path == "": 1243 | config_path = os.readlink(os.path.join(git_base_path, ".git", "config")) 1244 | original_dir_path = os.path.dirname(os.path.dirname(config_path)) 1245 | 1246 | return original_dir_path 1247 | 1248 | 1249 | def get_pr_stats(repo_name, pull_request_ID): 1250 | if pull_request_ID != None: 1251 | try: 1252 | pull_request_ID = int(pull_request_ID) 1253 | pull_request = get_pull_request(repo_name, pull_request_ID) 1254 | except Exception: 1255 | pull_request = pull_request_ID 1256 | 1257 | display_pull_request_minimal(pull_request) 1258 | 1259 | branch_name = build_branch_name(pull_request) 1260 | ret = os.system("git show-ref --verify -q refs/heads/%s" % branch_name) 1261 | 1262 | if ret != 0: 1263 | branch_name = fetch_pull_request(pull_request, repo_name) 1264 | 1265 | ret = os.system("git show-ref --verify -q refs/heads/%s" % branch_name) 1266 | 1267 | if ret != 0: 1268 | raise UserWarning("Fetch failed") 1269 | 1270 | merge_base = ( 1271 | os.popen("git merge-base %s %s" % (options["update-branch"], branch_name)) 1272 | .read() 1273 | .strip() 1274 | ) 1275 | 1276 | shortstat = ( 1277 | os.popen( 1278 | "git --no-pager diff --shortstat {0}..{1}".format( 1279 | merge_base, branch_name 1280 | ) 1281 | ) 1282 | .read() 1283 | .strip() 1284 | ) 1285 | stat_fragments = shortstat.split(", ") 1286 | stats_arr = shortstat.split(" ") 1287 | 1288 | has_color = False 1289 | for index, frag in enumerate(stat_fragments): 1290 | color_type = None 1291 | if "changed" in frag: 1292 | color_type = "total" 1293 | elif "insertions" in frag: 1294 | color_type = "added" 1295 | elif "deletions" in frag: 1296 | color_type = "deleted" 1297 | 1298 | if color_type: 1299 | has_color = True 1300 | stat_fragments[index] = color_text(frag, "stats-%s" % color_type) 1301 | 1302 | if has_color: 1303 | shortstat = ", ".join(stat_fragments) 1304 | 1305 | dels = 0 1306 | if len(stats_arr) > 5: 1307 | dels = int(stats_arr[5]) 1308 | 1309 | stats = (int(stats_arr[3]) + dels) / int(stats_arr[0]) 1310 | 1311 | stats = color_text( 1312 | "Average %d change(s) per file" % stats, "stats-average-change" 1313 | ) 1314 | 1315 | ret = ( 1316 | os.popen( 1317 | r"echo '{2}, {3}' && git diff --numstat --pretty='%H' --no-renames {0}..{1} | xargs -0n1 echo -n | cut -f 3- | sed -e 's/^.*\.\(.*\)$/\\1/' | sort | uniq -c | tr '\n' ',' | sed 's/,$//'".format( 1318 | merge_base, branch_name, shortstat, stats 1319 | ) 1320 | ) 1321 | .read() 1322 | .strip() 1323 | ) 1324 | 1325 | print(ret) 1326 | 1327 | stats_footer = options["stats-footer"] 1328 | 1329 | if stats_footer: 1330 | committers = ( 1331 | os.popen( 1332 | "git log {0}..{1} --pretty='%an' --reverse | awk ' !x[$0]++'".format( 1333 | merge_base, branch_name 1334 | ) 1335 | ) 1336 | .read() 1337 | .strip() 1338 | ) 1339 | committers = committers.split(os.linesep) 1340 | committers = ", ".join(committers) 1341 | 1342 | fn = False 1343 | 1344 | if stats_footer.startswith("`"): 1345 | stats_footer = stats_footer[1:] 1346 | fn = True 1347 | 1348 | footer_tpl = Template(stats_footer) 1349 | 1350 | committers = committers.decode("utf-8") 1351 | 1352 | pr_obj = pull_request.copy() 1353 | pr_obj.update( 1354 | { 1355 | "merge_base": merge_base[0:8], 1356 | "branch_name": branch_name, 1357 | "committers": committers, 1358 | "NEWLINE": os.linesep, 1359 | } 1360 | ) 1361 | 1362 | footer_result = footer_tpl.safe_substitute(**pr_obj) 1363 | 1364 | if fn: 1365 | footer_result = ( 1366 | os.popen(footer_result.encode("utf-8")) 1367 | .read() 1368 | .strip() 1369 | .decode("utf-8") 1370 | ) 1371 | 1372 | print(footer_result) 1373 | 1374 | print 1375 | else: 1376 | pull_requests = get_pull_requests(repo_name, options["filter-by-update-branch"]) 1377 | 1378 | for pull_request in pull_requests: 1379 | get_pr_stats(repo_name, pull_request) 1380 | 1381 | 1382 | def get_pull_request(repo_name, pull_request_ID): 1383 | """Returns information retrieved from github about the pull request""" 1384 | 1385 | url = get_api_url("repos/%s/pulls/%s" % (repo_name, pull_request_ID)) 1386 | 1387 | data = github_json_request(url) 1388 | 1389 | return data 1390 | 1391 | 1392 | def get_pull_request_ID(branch_name): 1393 | """Returns the pull request number of the branch with the name""" 1394 | 1395 | m = re.search(r"^pull-request-(\d+)", branch_name) 1396 | 1397 | pull_request_ID = None 1398 | 1399 | if m and m.group(1) != "": 1400 | pull_request_ID = int(m.group(1)) 1401 | 1402 | return pull_request_ID 1403 | 1404 | 1405 | def get_pull_requests(repo_name, filter_by_update_branch=False): 1406 | """Returns information retrieved from github about the open pull requests on 1407 | the repository""" 1408 | 1409 | url = get_api_url("repos/%s/pulls" % repo_name) 1410 | 1411 | pulls = github_json_request(url) 1412 | 1413 | if filter_by_update_branch: 1414 | update_branch = options["update-branch"] 1415 | 1416 | pull_requests = [pull for pull in pulls if pull["base"]["ref"] == update_branch] 1417 | else: 1418 | pull_requests = pulls 1419 | 1420 | return pull_requests 1421 | 1422 | 1423 | def get_repo_name_for_remote(remote_name): 1424 | """Returns the repository name for the remote with the name""" 1425 | 1426 | remotes = os.popen("git remote -v").read() 1427 | 1428 | m = re.search( 1429 | r"^%s[^\n]+?github\.com[^\n]*?[:/]([^\n]+?)\.git" % remote_name, 1430 | remotes, 1431 | re.MULTILINE, 1432 | ) 1433 | 1434 | if m is not None and m.group(1) != "": 1435 | return m.group(1) 1436 | 1437 | 1438 | def get_repo_url(pull_request, repo_name, force=False): 1439 | """Returns the git URL of the repository the pull request originated from""" 1440 | 1441 | if force is False: 1442 | repo_url = "git@github.com:%s.git" % repo_name 1443 | else: 1444 | repo_url = pull_request["head"]["repo"]["html_url"].replace("https", "git") 1445 | private_repo = pull_request["head"]["repo"]["private"] 1446 | 1447 | if private_repo: 1448 | repo_url = pull_request["head"]["repo"]["ssh_url"] 1449 | 1450 | return repo_url 1451 | 1452 | 1453 | def get_tmp_path(filename): 1454 | return TMP_PATH % filename 1455 | 1456 | 1457 | def get_user_email(github_user_info): 1458 | email = None 1459 | 1460 | if "email" in github_user_info: 1461 | email = github_user_info["email"] 1462 | 1463 | if email != None and email.endswith("@liferay.com"): 1464 | email = email[:-12] 1465 | 1466 | if email.isdigit(): 1467 | email = None 1468 | else: 1469 | email = None 1470 | 1471 | if email == None: 1472 | if ( 1473 | "name" in github_user_info 1474 | and github_user_info["name"] != None 1475 | and " " in github_user_info["name"] 1476 | ): 1477 | email = github_user_info["name"].lower() 1478 | email = email.replace(" ", ".") 1479 | email = email.replace("(", ".") 1480 | email = email.replace(")", ".") 1481 | 1482 | email = re.sub(r"\.+", ".", email) 1483 | 1484 | # Unicode characters usually do not appear in Liferay emails, so 1485 | # we'll replace them with the closest ASCII equivalent 1486 | 1487 | email = email.replace(u"\u00e1", "a") 1488 | email = email.replace(u"\u00e3", "a") 1489 | email = email.replace(u"\u00e9", "e") 1490 | email = email.replace(u"\u00f3", "o") 1491 | email = email.replace(u"\u00fd", "y") 1492 | email = email.replace(u"\u0107", "c") 1493 | email = email.replace(u"\u010d", "c") 1494 | email = email.replace(u"\u0151", "o") 1495 | email = email.replace(u"\u0161", "s") 1496 | 1497 | return email 1498 | 1499 | 1500 | def get_work_dir(): 1501 | global _work_dir 1502 | 1503 | if _work_dir == None: 1504 | symbolic_ref = ( 1505 | os.popen("git symbolic-ref HEAD").read().strip().replace("refs/heads/", "") 1506 | ) 1507 | work_dir_global = options["work-dir"] 1508 | 1509 | work_dir_option = None 1510 | 1511 | if symbolic_ref: 1512 | work_dir_option = "work-dir-%s" % symbolic_ref 1513 | 1514 | if work_dir_option: 1515 | _work_dir = ( 1516 | os.popen("git config git-pull-request.%s" % work_dir_option) 1517 | .read() 1518 | .strip() 1519 | ) 1520 | options[work_dir_option] = _work_dir 1521 | 1522 | if not _work_dir or not os.path.exists(_work_dir): 1523 | _work_dir = False 1524 | 1525 | if not _work_dir: 1526 | if work_dir_global and os.path.exists(work_dir_global): 1527 | _work_dir = work_dir_global 1528 | else: 1529 | _work_dir = False 1530 | 1531 | return _work_dir 1532 | 1533 | 1534 | def github_json_request(url, params=None): 1535 | data = json.loads(github_request(url, params)) 1536 | 1537 | return data 1538 | 1539 | 1540 | def github_request(url, params=None, token=None): 1541 | headers = { 1542 | "Accept" : "application/vnd.github.v3+json" 1543 | } 1544 | 1545 | bearer_token = token if token else auth_token 1546 | 1547 | headers["Authorization"] = "Bearer %s" % (bearer_token) 1548 | 1549 | encode_data = params 1550 | 1551 | if encode_data: 1552 | if not isinstance(encode_data, str): 1553 | encode_data = json.dumps(params).encode('utf-8') 1554 | 1555 | if DEBUG: 1556 | print(url) 1557 | 1558 | response = None 1559 | 1560 | try: 1561 | http = urllib3.PoolManager() 1562 | 1563 | if encode_data: 1564 | response = http.request("POST", url, body=encode_data, headers=headers) 1565 | else: 1566 | response = http.request("GET", url, headers=headers) 1567 | 1568 | except Exception: 1569 | if response.status == 401 and auth_token: 1570 | raise UserWarning( 1571 | 'Could not authorize you to connect with Github. Try running "git config --global --unset github.oauth-token" and running your command again to reauthenticate.' 1572 | ) 1573 | 1574 | raise UserWarning("Could not authorize you to connect with Github.") 1575 | 1576 | data = response.data.decode("utf-8") 1577 | 1578 | MAP_RESPONSE[url] = response 1579 | 1580 | if data == "": 1581 | raise UserWarning("Invalid response from github") 1582 | 1583 | return data 1584 | 1585 | 1586 | def in_work_dir(): 1587 | git_base_path = get_git_base_path() 1588 | 1589 | work_dir = get_work_dir() 1590 | 1591 | return ( 1592 | isinstance(work_dir, str) 1593 | and git_base_path.lower() == work_dir.lower() 1594 | and os.path.islink(os.path.join(git_base_path, ".git", "config")) 1595 | ) 1596 | 1597 | 1598 | def load_options(): 1599 | all_config = os.popen("git config -l").read().strip() 1600 | git_base_path = os.popen("git rev-parse --show-toplevel").read().strip() 1601 | 1602 | path_prefix = "%s." % git_base_path 1603 | 1604 | overrides = {} 1605 | 1606 | matches = re.findall( 1607 | r"^git-pull-request\.([^=]+)=([^\n]*)$", all_config, re.MULTILINE 1608 | ) 1609 | 1610 | for k in matches: 1611 | key = k[0] 1612 | value = k[1] 1613 | 1614 | if value.lower() in ("f", "false", "no"): 1615 | value = False 1616 | elif value.lower() in ("t", "true", "yes"): 1617 | value = True 1618 | elif value.lower() in ("", "none", "null", "nil"): 1619 | value = None 1620 | 1621 | if key.find(path_prefix) == -1: 1622 | options[key] = value 1623 | else: 1624 | key = key.replace(path_prefix, "") 1625 | overrides[key] = value 1626 | 1627 | options.update(overrides) 1628 | 1629 | 1630 | def load_users(filename): 1631 | try: 1632 | github_users_file = open(filename, "r") 1633 | except IOError: 1634 | print("File %s could not be found. Using email names will not be available. Run the update-users command to enable this functionality" % filename) 1635 | return {} 1636 | 1637 | github_users = json.load(github_users_file) 1638 | 1639 | github_users_file.close() 1640 | 1641 | return github_users 1642 | 1643 | 1644 | def log(*args): 1645 | for arg in args: 1646 | print(json.dumps(arg, sort_keys=True, indent=4)) 1647 | print("/---") 1648 | 1649 | 1650 | def lookup_alias(key): 1651 | user_alias = key 1652 | 1653 | try: 1654 | if users and (key in users) and users[key]: 1655 | user_alias = users[key] 1656 | except Exception as e: 1657 | pass 1658 | 1659 | return user_alias 1660 | 1661 | 1662 | def main(): 1663 | global DEBUG 1664 | global FORCE_COLOR 1665 | 1666 | FORCE_COLOR = False 1667 | 1668 | # parse command line options 1669 | try: 1670 | opts, args = getopt.gnu_getopt( 1671 | sys.argv[1:], 1672 | "hqar:u:l:b:", 1673 | [ 1674 | "help", 1675 | "quiet", 1676 | "all", 1677 | "repo=", 1678 | "reviewer=", 1679 | "update", 1680 | "no-update", 1681 | "user=", 1682 | "update-branch=", 1683 | "authenticate", 1684 | "debug", 1685 | "force-color", 1686 | ], 1687 | ) 1688 | except getopt.GetoptError as e: 1689 | raise UserWarning("%s\nFor help use --help" % e) 1690 | 1691 | arg_length = len(args) 1692 | command = "show" 1693 | 1694 | if arg_length > 0: 1695 | command = args[0] 1696 | 1697 | if command == "help": 1698 | command_help() 1699 | sys.exit(0) 1700 | 1701 | # load git options 1702 | load_options() 1703 | 1704 | global users, DEFAULT_USERNAME 1705 | global _work_dir 1706 | global auth_token 1707 | 1708 | DEBUG = options["debug-mode"] 1709 | 1710 | _work_dir = None 1711 | 1712 | repo_name = None 1713 | reviewer_repo_name = None 1714 | 1715 | username = os.popen("git config github.user").read().strip() 1716 | 1717 | auth_token = os.popen("git config github.oauth-token").read().strip() 1718 | 1719 | fetch_auto_update = options["fetch-auto-update"] 1720 | 1721 | info_user = username 1722 | submitOpenGitHub = options["submit-open-github"] 1723 | 1724 | # manage github usernames 1725 | users_alias_file = ( 1726 | os.popen("git config git-pull-request.users-alias-file").read().strip() 1727 | ) 1728 | 1729 | if len(users_alias_file) == 0: 1730 | users_alias_file = "git-pull-request.users" 1731 | 1732 | if command != "update-users": 1733 | users = load_users(users_alias_file) 1734 | 1735 | # process options 1736 | for o, a in opts: 1737 | if o in ("-h", "--help"): 1738 | command_help() 1739 | sys.exit(0) 1740 | elif o in ("-q", "--quiet"): 1741 | submitOpenGitHub = False 1742 | elif o in ("-a", "--all"): 1743 | options["filter-by-update-branch"] = False 1744 | elif o in ("-r", "--repo"): 1745 | if re.search("/", a): 1746 | repo_name = a 1747 | else: 1748 | repo_name = get_repo_name_for_remote(a) 1749 | elif o in ("-b", "--update-branch"): 1750 | options["update-branch"] = a 1751 | elif o in ("-u", "--user", "--reviewer"): 1752 | reviewer_repo_name = a 1753 | info_user = lookup_alias(a) 1754 | elif o == "--update": 1755 | fetch_auto_update = True 1756 | elif o == "--no-update": 1757 | fetch_auto_update = False 1758 | elif o == "--authenticate": 1759 | username = "" 1760 | auth_token = "" 1761 | elif o == "--debug": 1762 | DEBUG = True 1763 | elif o == "--force-color": 1764 | FORCE_COLOR = True 1765 | 1766 | if len(auth_token) == 0: 1767 | token = getpass.getpass("Github token: ").strip() 1768 | 1769 | # check if the token is valid 1770 | github_request("https://api.github.com/user/repos", None, token) 1771 | 1772 | auth_token = token 1773 | 1774 | os.system("git config --global github.oauth-token %s" % auth_token) 1775 | 1776 | # get repo name from git config 1777 | if repo_name is None or repo_name == "": 1778 | repo_name = get_default_repo_name() 1779 | 1780 | if (not reviewer_repo_name) and (command == "submit"): 1781 | reviewer_repo_name = os.popen("git config github.reviewer").read().strip() 1782 | 1783 | if reviewer_repo_name: 1784 | reviewer_repo_name = lookup_alias(reviewer_repo_name) 1785 | 1786 | if command != 'forward' and command != "submit": 1787 | repo_name = reviewer_repo_name + "/" + repo_name.split("/")[1] 1788 | 1789 | DEFAULT_USERNAME = username 1790 | 1791 | # process arguments 1792 | if command == "show": 1793 | command_show(repo_name) 1794 | elif arg_length > 0: 1795 | if command == "alias": 1796 | if arg_length >= 2: 1797 | command_alias(args[1], args[2], users_alias_file) 1798 | elif command == "close": 1799 | if arg_length >= 2: 1800 | comment = args[1] 1801 | pull_request_ID = comment 1802 | 1803 | if comment.isdigit(): 1804 | comment = "" 1805 | 1806 | if arg_length == 3: 1807 | comment = args[2] 1808 | 1809 | print(color_text("Closing pull request", "status")) 1810 | close_pull_request(repo_name, pull_request_ID, comment) 1811 | else: 1812 | command_close(repo_name, comment) 1813 | else: 1814 | command_close(repo_name) 1815 | elif command in ("continue-update", "cu"): 1816 | command_continue_update() 1817 | elif command == "fetch": 1818 | command_fetch(repo_name, args[1], fetch_auto_update) 1819 | elif command == "fetch-all": 1820 | command_fetch_all(repo_name) 1821 | elif command == "forward": 1822 | command_forward(repo_name, args[1], username, reviewer_repo_name) 1823 | elif command == "help": 1824 | command_help() 1825 | elif command == "info": 1826 | command_info(info_user) 1827 | elif command == "info-detailed": 1828 | command_info(info_user, True) 1829 | elif command == "merge": 1830 | if arg_length >= 2: 1831 | command_merge(repo_name, args[1]) 1832 | else: 1833 | command_merge(repo_name) 1834 | elif command == "open": 1835 | if arg_length >= 2: 1836 | command_open(repo_name, args[1]) 1837 | else: 1838 | command_open(repo_name) 1839 | elif command == "pull": 1840 | command_pull(repo_name) 1841 | elif command == "update-meta": 1842 | command_update_meta() 1843 | elif command == "submit": 1844 | pull_body = None 1845 | pull_title = None 1846 | 1847 | if arg_length >= 2: 1848 | pull_body = args[1] 1849 | 1850 | if arg_length >= 3: 1851 | pull_title = args[2] 1852 | 1853 | command_submit( 1854 | repo_name, 1855 | username, 1856 | reviewer_repo_name, 1857 | pull_body, 1858 | pull_title, 1859 | submitOpenGitHub, 1860 | ) 1861 | elif command == "update": 1862 | if arg_length >= 2: 1863 | command_update(repo_name, args[1]) 1864 | else: 1865 | command_update(repo_name, options["update-branch"]) 1866 | elif command == "update-users": 1867 | command_update_users(users_alias_file) 1868 | elif command == "show-alias": 1869 | if arg_length >= 2: 1870 | command_show_alias(args[1]) 1871 | elif command == "comment": 1872 | command_comment(repo_name, *args[1:]) 1873 | elif command == "stats" or args[0] == "stat": 1874 | pull_request_ID = None 1875 | 1876 | if arg_length >= 2: 1877 | pull_request_ID = args[1] 1878 | 1879 | get_pr_stats(repo_name, pull_request_ID) 1880 | else: 1881 | command_fetch(repo_name, args[0], fetch_auto_update) 1882 | 1883 | 1884 | def meta(key=None, value=None): 1885 | branch_name = get_current_branch_name(False) 1886 | 1887 | pull_request_ID = get_pull_request_ID(branch_name) 1888 | 1889 | val = None 1890 | 1891 | if pull_request_ID is not None: 1892 | meta_data_path = get_tmp_path("git-pull-request-treeish-%s" % pull_request_ID) 1893 | 1894 | try: 1895 | f = open(meta_data_path, "r+") 1896 | current_value = json.load(f) 1897 | current_obj = current_value 1898 | 1899 | val = current_value 1900 | 1901 | if key != None: 1902 | pieces = key.split(".") 1903 | 1904 | key = pieces.pop() 1905 | 1906 | for word in pieces: 1907 | current_obj = current_obj[word] 1908 | 1909 | if value == None: 1910 | if key in current_obj: 1911 | val = current_obj[key] 1912 | else: 1913 | val = "" 1914 | 1915 | if value != None: 1916 | val = value 1917 | current_obj[key] = value 1918 | f.seek(0) 1919 | f.truncate(0) 1920 | json.dump(current_value, f) 1921 | 1922 | f.close() 1923 | 1924 | return val 1925 | 1926 | except Exception: 1927 | log("Could not update '%s' with '%s'" % (key, value)) 1928 | 1929 | 1930 | def open_URL(url): 1931 | if os.popen("command -v open").read().strip() != "": 1932 | ret = os.system('open -g "%s" 2>/dev/null' % url) 1933 | 1934 | if ret != 0: 1935 | os.system('open "%s"' % url) 1936 | elif os.popen("command -v cygstart").read().strip() != "": 1937 | os.system('cygstart "%s"' % url) 1938 | else: 1939 | try: 1940 | webbrowser.open_new_tab(url) 1941 | except Exception: 1942 | pass 1943 | 1944 | 1945 | def post_comment(repo_name, pull_request_ID, comment): 1946 | url = get_api_url("repos/%s/issues/%s/comments" % (repo_name, pull_request_ID)) 1947 | params = {"body": comment} 1948 | 1949 | github_json_request(url, params) 1950 | 1951 | 1952 | def strip_empty_lines(text): 1953 | lines = text.splitlines() 1954 | while lines and not lines[0].strip(): 1955 | lines.pop(0) 1956 | while lines and not lines[-1].strip(): 1957 | lines.pop() 1958 | return "\n".join(lines) 1959 | 1960 | 1961 | def strip_html_tags(html_raw): 1962 | html = "" 1963 | quote = False 1964 | tag = False 1965 | 1966 | for line in re.sub(r"([\s\S]*?)", "", html_raw): 1967 | if line == '<' and not quote: 1968 | tag = True 1969 | elif line == '>' and not quote: 1970 | tag = False 1971 | elif (line == '"' or line == "'") and tag: 1972 | quote = not quote 1973 | elif not tag: 1974 | html += line 1975 | 1976 | return html 1977 | 1978 | 1979 | def update_branch(branch_name): 1980 | if in_work_dir(): 1981 | raise UserWarning( 1982 | "Cannot perform an update from within the work directory.\nIf you are done fixing conflicts run 'gitpr continue-update' to complete the update." 1983 | ) 1984 | 1985 | work_dir = get_work_dir() 1986 | 1987 | if work_dir: 1988 | original_dir_path = get_git_base_path() 1989 | 1990 | print(color_text("Switching to work directory %s" % work_dir, "status")) 1991 | os.chdir(work_dir) 1992 | 1993 | f = open(os.path.join(work_dir, ".git", "original_dir_path"), "wb") 1994 | f.write(original_dir_path) 1995 | f.close() 1996 | 1997 | ret = os.system("git reset --hard && git clean -f") 1998 | if ret != 0: 1999 | raise UserWarning("Cleaning up work directory failed, update not performed") 2000 | 2001 | ret = os.system("git checkout %s" % branch_name) 2002 | if ret != 0: 2003 | if work_dir: 2004 | raise UserWarning( 2005 | "Could not checkout %s in the work directory, update not performed" 2006 | % branch_name 2007 | ) 2008 | else: 2009 | raise UserWarning( 2010 | "Could not checkout %s, update not performed" % branch_name 2011 | ) 2012 | 2013 | update_branch_option = options["update-branch"] 2014 | 2015 | ret = os.system("git %(update-method)s %(update-branch)s" % (options)) 2016 | 2017 | if ret != 0: 2018 | if work_dir: 2019 | chdir(work_dir) 2020 | raise UserWarning( 2021 | "Updating %s from %s failed\nResolve conflicts and 'git add' files, then run 'gitpr continue-update'" 2022 | % (branch_name, update_branch_option) 2023 | ) 2024 | 2025 | complete_update(branch_name) 2026 | 2027 | 2028 | def update_meta(): 2029 | branch_name = get_current_branch_name() 2030 | update_branch_option = options["update-branch"] 2031 | parent_commit = ( 2032 | os.popen("git merge-base %s %s" % (update_branch_option, branch_name)) 2033 | .read() 2034 | .strip()[0:10] 2035 | ) 2036 | head_commit = os.popen("git rev-parse HEAD").read().strip()[0:10] 2037 | 2038 | updated = {"parent_commit": parent_commit, "head_commit": head_commit} 2039 | 2040 | meta("updated", updated) 2041 | 2042 | if parent_commit == head_commit: 2043 | branch_treeish = head_commit 2044 | else: 2045 | branch_treeish = "%s..%s" % (parent_commit, head_commit) 2046 | 2047 | print(color_text("Original commits: %s" % branch_treeish, "status")) 2048 | 2049 | return branch_treeish 2050 | 2051 | 2052 | if __name__ == "__main__": 2053 | try: 2054 | main() 2055 | except UserWarning as e: 2056 | print(color_text(e, "error")) 2057 | sys.exit(1) --------------------------------------------------------------------------------