diff --git a/README.md b/README.md index 48b61ec..9c17cab 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,23 @@ git open --issue git open -i # If this branch is named like issue/#123, this will open the corresponding # issue in the repo website + # Supports multiple formats: issue/123, bugfix-123, #123, feature/123, fix-123, etc. + +git open --pull-request +git open -r + # Open the pull requests / merge requests page for the repository + # Supports GitHub, GitLab, Bitbucket, Azure DevOps, and cnb.cool + +git open --path + # Open the current directory path in the repo website + # Useful when you want to browse the current folder in the browser git open --print git open -p # Only print the url at the terminal, but don't open it ``` -(`git open` works with these [hosted repo providers](#supported-remote-repositories), `git open --issue` currently only works with GitHub, Visual Studio Team Services and Team Foundation Server) +(`git open` works with these [hosted repo providers](#supported-remote-repositories), `git open --issue` currently only works with GitHub, GitLab, Visual Studio Team Services and Team Foundation Server) ### Examples @@ -39,14 +49,24 @@ $ git open someremote somebranch # opens https://github.com/PROVIDED_REMOTE_USER/CURRENT_REPO/tree/PROVIDED_BRANCH $ git open --issue -# If branches use naming convention of issues/#123, +# If branches use naming convention of issues/#123, bugfix-456, feature-789, etc. # opens https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/issues/123 +$ git open --pull-request +# opens https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/pulls (GitHub) +# or https://gitlab.example.com/user/repo/-/merge_requests (GitLab) + +$ git open --path +# opens https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/tree/CURRENT_BRANCH/current/directory + $ git open --print # prints https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/tree/CURRENT_BRANCH $ git open --suffix pulls # opens https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/pulls + +$ git open --file README.md +# opens https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/tree/CURRENT_BRANCH/README.md ``` ## Installation diff --git a/git-open b/git-open index 8581e6c..f1d1c41 100755 --- a/git-open +++ b/git-open @@ -18,8 +18,10 @@ git open [remote] [branch] Available options are c,commit! open current commit i,issue! open issues page +r,pull-request! open pull request page s,suffix= append this suffix f,file= append this file +path! open current directory path p,print! just print the url " @@ -30,17 +32,22 @@ SUBDIRECTORY_OK='Yes' . "$(git --exec-path)/git-sh-setup" # Defaults is_commit=0 is_issue=0 +is_pull_request=0 protocol="https" print_only=0 suffix_flag="" file_flag="" +path_flag="" while test $# != 0; do case "$1" in --commit) is_commit=1;; --issue) is_issue=1;; + --pull-request) is_pull_request=1;; + -r) is_pull_request=1;; --suffix=*) suffix_flag="$1";; --file=*) file_flag="$1";; + --path) path_flag=1;; --print) print_only=1;; --) shift; break ;; esac @@ -52,16 +59,28 @@ IFS='=' read -ra suffix_flag <<< "$suffix_flag" function join_by { local IFS="$1"; shift; echo "$*"; } suffix=$(join_by "=" "${suffix_flag[@]:1}") -# parse file from file=value -IFS='=' read -ra file_flag <<< "$file_flag" -file=$(join_by "=" "${file_flag[@]:1}") - # are we in a git repo? if ! git rev-parse --is-inside-work-tree &>/dev/null; then echo "Not a git repository." 1>&2 exit 1 fi +# parse file from file=value +IFS='=' read -ra file_flag <<< "$file_flag" +file=$(join_by "=" "${file_flag[@]:1}") + +# handle --path: use current directory relative to repo root +path_suffix="" +if [[ "$path_flag" == "1" ]]; then + repo_root=$(git rev-parse --show-toplevel) + current_dir=$(pwd) + if [[ "$current_dir" != "$repo_root" ]]; then + # Get relative path from repo root + rel_path="${current_dir#"$repo_root"/}" + path_suffix="$rel_path" + fi +fi + # choose remote. priority to: provided argument, default in config, detected tracked remote, 'origin' branch=${2:-$(git symbolic-ref -q --short HEAD)} upstream_branch_full_name=$(git config "branch.$branch.merge") @@ -183,10 +202,47 @@ remote_ref=${upstream_branch:-${branch:-$(git describe --tags --exact-match 2>/d # Split arguments on '/' IFS='/' read -r -a pathargs <<<"$urlpath" +# Extract issue number from branch name +# Supports formats: issue/123, bugfix-123, #123, issues/123, feature/123, fix-123, etc. +function extract_issue_number() { + local branch_name="$1" + local issue_num="" + + # Try various patterns: issue/123, bugfix-123, #123, etc. + if [[ "$branch_name" =~ (^|[-/])(issues?|bugfix|feature|fix|hotfix)?[-/]?[#]?([0-9]+) ]]; then + issue_num="${BASH_REMATCH[3]}" + elif [[ "$branch_name" =~ ^#?([0-9]+)$ ]]; then + # Just a number like #123 or 123 + issue_num="${BASH_REMATCH[1]}" + fi + + echo "$issue_num" +} + if (( is_issue )); then - # For issues, take the numbers and preprend 'issues/' - [[ $remote_ref =~ [0-9]+ ]] - providerBranchRef="/issues/${BASH_REMATCH[0]}" + issue_number=$(extract_issue_number "$remote_ref") + if [[ -z "$issue_number" ]]; then + echo "Could not extract issue number from branch name: $remote_ref" 1>&2 + echo "Supported formats: issue/123, bugfix-123, #123, 123, etc." 1>&2 + exit 1 + fi + providerBranchRef="/issues/$issue_number" +elif (( is_pull_request )); then + # Generate pull request URL based on platform + if [[ "$domain" == 'github.com' || "$domain" == *'github'* ]]; then + providerBranchRef="/pulls" + elif [[ "$domain" == 'gitlab.com' || "$domain" == *'gitlab'* || "$forge" == 'gitlab' ]]; then + providerBranchRef="/-/merge_requests" + elif [[ "$domain" == 'bitbucket.org' || "$domain" == *'bitbucket'* ]]; then + providerBranchRef="/pull-requests" + elif [[ "$domain" == *'visualstudio.com'* || "$domain" == *'azure.com'* || "$domain" == *'dev.azure.com'* ]]; then + providerBranchRef="/_git/pullrequests" + elif [[ "$domain" =~ cnb\.cool$ ]]; then + providerBranchRef="/-/merge_requests" + else + # Default to GitHub-style /pulls + providerBranchRef="/pulls" + fi else # Make # and % characters url friendly # github.com/paulirish/git-open/pull/24 @@ -198,7 +254,11 @@ else fi fi -if [[ "$domain" == 'bitbucket.org' ]]; then +# Skip provider-specific branch ref modifications for pull-request only +# (issue handling is provider-specific and needs to be processed below) +if (( is_pull_request )); then + : # Do nothing, keep the providerBranchRef set earlier +elif [[ "$domain" == 'bitbucket.org' ]]; then providerBranchRef="/src/$remote_ref" elif [[ "${#pathargs[@]}" -ge 3 && ${pathargs[${#pathargs[@]} - 3]} == 'scm' ]]; then # Bitbucket server always has /scm/ as the third to last segment in the url path, e.g. /scm/ppp/test-repo.git @@ -257,12 +317,18 @@ elif [[ "$domain" =~ amazonaws\.com$ ]]; then providerBranchRef="${providerBranchRef##*/}/--/" elif [[ "$domain" =~ cnb\.cool$ ]]; then # cnb.cool - # Replace URL path. - # Ex. repos/example -> repos/example/- - urlpath="$urlpath/-" - if [[ $remote_ref = "master" ]]; then - # repos/example/tree/master -> repos/example/-/tree/master - urlpath="$urlpath$providerBranchRef" + if (( is_issue || is_pull_request )); then + # For issue and pull-request, add the path prefix to urlpath + # providerBranchRef already starts with /issues/ or /-/merge_requests + urlpath="$urlpath/-" + else + # Replace URL path. + # Ex: repos/example -> repos/example/- + urlpath="$urlpath/-" + if [[ $remote_ref = "master" ]]; then + # repos/example/tree/master -> repos/example/-/tree/master + urlpath="$urlpath$providerBranchRef" + fi fi fi openurl="$protocol://$domain/$urlpath" @@ -270,7 +336,13 @@ openurl="$protocol://$domain/$urlpath" if (( is_commit )); then sha=$(git rev-parse HEAD) openurl="$openurl/commit/$sha" -elif [[ $remote_ref != "master" || "$file" ]]; then +elif (( is_pull_request )); then + # For pull requests, just add the PR path + openurl="$openurl$providerBranchRef" +elif (( is_issue )); then + # For issues, just add the issue path + openurl="$openurl$providerBranchRef" +elif [[ $remote_ref != "master" || "$file" || "$path_suffix" ]]; then # simplify URL for master openurl="$openurl$providerBranchRef" fi @@ -284,6 +356,11 @@ if [ "$file" ]; then openurl="$openurl/$absfile" fi +# Add path_suffix from --path option +if [ "$path_suffix" ]; then + openurl="$openurl/$path_suffix" +fi + if [ "$suffix" ]; then openurl="$openurl/$suffix" fi diff --git a/git-open.1.md b/git-open.1.md index 504669a..93384a7 100644 --- a/git-open.1.md +++ b/git-open.1.md @@ -3,7 +3,7 @@ ## SYNOPSIS -`git open` [--issue] [--commit] [--suffix some_suffix] [remote-name] [branch-name] +`git open` [--issue] [--commit] [--pull-request] [--path] [--suffix some_suffix] [--file some_file] [remote-name] [branch-name] ## DESCRIPTION @@ -15,14 +15,40 @@ git hosting services are supported. ## OPTIONS `-c`, `--commit` - Open the current commit. See `EXAMPLES` for more information. + Open the current commit. See `EXAMPLES` for more information. Only tested with GitHub & GitLab. `-i`, `--issue` - Open the current issue. When the name of the current branch matches the right pattern, - it will open the webpage with that issue. See `EXAMPLES` for more information. + Open the current issue. When the name of the current branch matches the right pattern, + it will open the webpage with that issue. See `EXAMPLES` for more information. This only works on GitHub, GitLab, Visual Studio Team Services and Team Foundation Server at the moment. + Supported branch name formats: + - `issue/123`, `issues/123` + - `bugfix-123`, `bugfix/123` + - `feature-123`, `feature/123` + - `fix-123`, `fix/123` + - `hotfix-123`, `hotfix/123` + - `#123` (just the issue number with hash) + +`-r`, `--pull-request` + Open the pull requests / merge requests page for the repository. + The URL format is automatically determined based on the git hosting service: + - GitHub: `/pulls` + - GitLab: `/-/merge_requests` + - Bitbucket: `/pull-requests` + - Azure DevOps: `/_git/pullrequests` + - cnb.cool: `/-/merge_requests` + +`--path` + Open the current directory path in the repo website. This uses the current + working directory relative to the repository root to construct the URL. + Useful when you want to browse the current folder in the browser. + +`-f`, `--file` some_file + Append the given file path to the URL. The file path is relative to the + repository root. Use `--path` to automatically use the current directory. + `-s`, `--suffix` some_suffix Append the given suffix to the url @@ -56,9 +82,28 @@ It opens https://github.com/PROVIDED_REMOTE_USER/CURRENT_REPO/tree/PROVIDED_BRAN git open --issue ``` -If branches use naming convention of `issues/#123`, it opens +If branches use naming convention of `issues/#123`, `bugfix-123`, `feature/456`, etc., it opens https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/issues/123 +```sh +git open --pull-request +``` + +It opens the URL https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/pulls (GitHub) +or https://gitlab.example.com/user/repo/-/merge_requests (GitLab) + +```sh +git open --path +``` + +It opens the URL https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/tree/CURRENT_BRANCH/current/directory + +```sh +git open --file README.md +``` + +It opens the URL https://github.com/TRACKED_REMOTE_USER/CURRENT_REPO/tree/CURRENT_BRANCH/README.md + ```sh git open --suffix pulls ``` diff --git a/test/git-open.bats b/test/git-open.bats index e891be5..34e4196 100755 --- a/test/git-open.bats +++ b/test/git-open.bats @@ -666,6 +666,140 @@ setup() { assert_output "https://cnb.cool/repos/repo/-/issues/10" } +## +## Enhanced --issue with multiple branch name formats +## + +@test "gh: git open --issue with bugfix- prefix" { + git remote set-url origin "github.com:paulirish/git-open.git" + git checkout -B "bugfix-123" + run ../git-open "--issue" + assert_output "https://github.com/paulirish/git-open/issues/123" +} + +@test "gh: git open --issue with feature/ prefix" { + git remote set-url origin "github.com:paulirish/git-open.git" + git checkout -B "feature/456" + run ../git-open "--issue" + assert_output "https://github.com/paulirish/git-open/issues/456" +} + +@test "gh: git open --issue with fix- prefix" { + git remote set-url origin "github.com:paulirish/git-open.git" + git checkout -B "fix-789" + run ../git-open "--issue" + assert_output "https://github.com/paulirish/git-open/issues/789" +} + +@test "gh: git open --issue with hotfix- prefix" { + git remote set-url origin "github.com:paulirish/git-open.git" + git checkout -B "hotfix-999" + run ../git-open "--issue" + assert_output "https://github.com/paulirish/git-open/issues/999" +} + +@test "gh: git open --issue with # prefix" { + git remote set-url origin "github.com:paulirish/git-open.git" + git checkout -B "#42" + run ../git-open "--issue" + assert_output "https://github.com/paulirish/git-open/issues/42" +} + +@test "gh: git open --issue error when no issue number found" { + git remote set-url origin "github.com:paulirish/git-open.git" + git checkout -B "some-random-branch" + run ../git-open "--issue" + [ "$status" -eq 1 ] + assert_output --partial "Could not extract issue number from branch name" + assert_output --partial "Supported formats: issue/123, bugfix-123, #123, 123, etc." +} + +## +## --pull-request tests +## + +@test "gh: git open --pull-request" { + git remote set-url origin "git@github.com:user/repo.git" + run ../git-open "--pull-request" + assert_output "https://github.com/user/repo/pulls" +} + +@test "gh: git open -r (short option)" { + git remote set-url origin "git@github.com:user/repo.git" + run ../git-open "-r" + assert_output "https://github.com/user/repo/pulls" +} + +@test "gitlab: git open --pull-request" { + git remote set-url origin "git@gitlab.com:user/repo.git" + run ../git-open "--pull-request" + assert_output "https://gitlab.com/user/repo/-/merge_requests" +} + +@test "gitlab: git open -r (short option)" { + git remote set-url origin "git@gitlab.com:user/repo.git" + run ../git-open "-r" + assert_output "https://gitlab.com/user/repo/-/merge_requests" +} + +@test "bitbucket: git open --pull-request" { + git remote set-url origin "git@bitbucket.org:user/repo.git" + run ../git-open "--pull-request" + assert_output "https://bitbucket.org/user/repo/pull-requests" +} + +@test "bitbucket: git open -r (short option)" { + git remote set-url origin "git@bitbucket.org:user/repo.git" + run ../git-open "-r" + assert_output "https://bitbucket.org/user/repo/pull-requests" +} + +@test "cnb: git open --pull-request" { + git remote set-url origin "https://cnb.cool/repos/repo" + run ../git-open "--pull-request" + assert_output "https://cnb.cool/repos/repo/-/merge_requests" +} + +@test "cnb: git open -r (short option)" { + git remote set-url origin "https://cnb.cool/repos/repo" + run ../git-open "-r" + assert_output "https://cnb.cool/repos/repo/-/merge_requests" +} + +## +## --path tests +## + +@test "gh: git open --path in root directory" { + git remote set-url origin "git@github.com:user/repo.git" + run ../git-open "--path" + assert_output "https://github.com/user/repo" +} + +@test "gh: git open --path in subdirectory" { + git remote set-url origin "git@github.com:user/repo.git" + mkdir -p subdir/nested + # Create a file in the directory so git tracks it + touch subdir/nested/.gitkeep + git add subdir + git commit -m "add subdir" -q + # Run from the subdirectory with explicit cd + run bash -c 'cd subdir/nested && "$0" --path' "$BATS_TEST_DIRNAME/../git-open" + assert_output "https://github.com/user/repo/tree/master/subdir/nested" +} + +@test "gh: git open --path combined with branch" { + git remote set-url origin "git@github.com:user/repo.git" + git checkout -B "feature-branch" + mkdir -p src/components + touch src/components/.gitkeep + git add src + git commit -m "add src dir" -q + # Run from the subdirectory with explicit cd + run bash -c 'cd src/components && "$0" --path' "$BATS_TEST_DIRNAME/../git-open" + assert_output "https://github.com/user/repo/tree/feature-branch/src/components" +} + teardown() { cd .. rm -rf "$foldername"