diff --git a/.github/workflows/trigger-homebrew.yml b/.github/workflows/trigger-homebrew.yml new file mode 100644 index 0000000..c0ddfb4 --- /dev/null +++ b/.github/workflows/trigger-homebrew.yml @@ -0,0 +1,25 @@ +name: Trigger Homebrew Formula Update + +on: + push: + tags: + - 'v*' + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Get latest completion version + id: completion_version + run: | + COMP_VERSION=$(curl -s https://api.github.com/repos/LBEM-CH/gitflow-lbem-completion/tags | jq -r '.[0].name') + echo "version=${COMP_VERSION}" >> $GITHUB_OUTPUT + echo "Latest completion version: ${COMP_VERSION}" + + - name: Trigger Homebrew formula update + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.HOMEBREW_DISPATCH_TOKEN }} + repository: LBEM-CH/homebrew-gitflow-lbem + event-type: main-release + client-payload: '{"version": "${{ github.ref_name }}", "completion_version": "${{ steps.completion_version.outputs.version }}"}' diff --git a/README.md b/README.md index 637867c..a988619 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,45 @@ -# git-flow (CJS Edition) +# git-flow (LBEM Edition) A collection of Git extensions to provide high-level repository operations for Vincent Driessen's [branching model](http://nvie.com/git-model "original -blog post"). This fork adds functionality not added to the original branch. +blog post"). + +## About LBEM Edition + +This is a fork of [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs) that adds workflow enhancements inspired by [git-town](https://www.git-town.com/). LBEM Edition stays close to the CJS version to easily pull upstream bugfixes while adding the following features: + +### Additional Commands + +- **`git flow feature sync`** / **`git flow bugfix sync`** - Sync your branch with the base branch (like `git-town sync`). Handles uncommitted changes automatically, supports rebase or merge strategy. +- **`git flow feature propose`** / **`git flow bugfix propose`** - Create a pull request for your branch using `gh` CLI (falls back to opening browser URL). +- **`git flow config export`** - Export gitflow settings to a `.gitflow` file for team sharing. + +### Configuration Enhancements + +- **`.gitflow` file support** - Store gitflow configuration in a shareable file. When a developer clones a repo with a `.gitflow` file and runs `git flow init`, settings are automatically imported. +- **Finish mode** - Configure how `finish` behaves: `classic` (traditional merge), `propose` (create PR), or `ask` (prompt each time). Set via `git flow config set finishmode `. +- **Sync strategy** - Choose `rebase` (default) or `merge` for sync operations. Set via `git flow config set syncstrategy `. + +### macOS Compatibility + +LBEM Edition includes fixes for macOS compatibility with BSD getopt. Install GNU getopt via Homebrew and set: +```bash +export FLAGS_GETOPT_CMD="/usr/local/opt/gnu-getopt/bin/getopt" +``` + +## Upstream Projects + +This project is based on and grateful to: + +- **[gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs)** - The CJS Edition that continues maintenance after AVH was archived +- **[gitflow-avh](https://github.com/petervanderdoes/gitflow-avh)** - The AVH Edition that extended the original gitflow +- **[nvie/gitflow](https://github.com/nvie/gitflow)** - The original gitflow implementation by Vincent Driessen + +## Why another git-flow fork? -## Why another git-flow fork The last commit to [gitflow-avh](https://github.com/petervanderdoes/gitflow-avh) was on May 23, 2019 and has been archived on Jun 19, 2023. Since 2019 there have -been a number of issues opened that have not be resolved. This fork will address -those outstanding issues and open PR's along with continuing to maintain the -git-flow branching model. +been a number of issues opened that have not be resolved. [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs) continues maintenance but we needed additional workflow features for our team. ## Getting started @@ -30,14 +60,14 @@ A quick cheatsheet was made by Daniel Kummer: ## Installing git-flow -See the Wiki for up-to-date [Installation Instructions](https://github.com/CJ-Systems/gitflow-cjs/wiki/Installation). +See the Wiki for up-to-date [Installation Instructions](https://github.com/LBEM-CH/gitflow-lbem/wiki/Installation). ## Integration with your shell For those who use the [Bash](https://www.gnu.org/software/bash/) or [ZSH](https://www.zsh.org/) -shell, you can use my [fork of git-flow-completion](https://github.com/petervanderdoes/git-flow-completion) -which includes several additions for git-flow (AVH Edition), or you can use the +shell, you can use our [fork of git-flow-completion](https://github.com/LBEM-CH/gitflow-lbem-completion) +which includes several additions for git-flow (CJS & AVH Edition), or you can use the original [git-flow-completion](https://github.com/bobthecow/git-flow-completion) project by [bobthecow](https://github.com/bobthecow). Both offer tab-completion for git-flow subcommands and branch names with my fork including tab-completion @@ -46,12 +76,13 @@ for the commands not found in the original git-flow. ## FAQ -* See the [FAQ](https://github.com/CJ-Systems/gitflow-cjs/wiki/FAQ) section +* See the [FAQ](https://github.com/LBEM-CH/gitflow-lbem/wiki/FAQ) section of the project Wiki. * Version Numbering Scheme. Starting with version 1.0, the project uses the following scheme: -\.\.\\ +\.\.\-lbem.\ * CJS is the acronym of "CJ Systems" +* LBEM is the acronym of "Laboratory of Biological Electron Microscopy" ## Please help out @@ -72,21 +103,32 @@ using the complete version number. ## Contributing -If submiting a new pull request addressing an already open issue with gitflow-avh please link the relevant issue in the description. For any new issues please see below +### Where to contribute? + +- **LBEM Edition features** (sync, propose, config export, .gitflow file support, finish mode): Please contribute to [gitflow-lbem](https://github.com/LBEM-CH/gitflow-lbem) +- **General gitflow bugs and features**: Please contribute to the upstream [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs) repository. We regularly pull in upstream changes. + +### Quick Start for LBEM Edition contributions -### Quick Start for new issues +* Fork and clone [gitflow-lbem](https://github.com/LBEM-CH/gitflow-lbem) +* Create a feature branch based off develop: `git flow feature start my-feature` +* Commit your changes to the local branch +* Push your feature branch: `git flow feature publish` +* Create a pull request against the `develop` branch -* Please fork and clone a local copy of [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs). -* Create a seperate issue branch based off develop. -* Commit commit you fix to the local branch. -* Please update your local copy with the latest devlop branch of [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs) -* Rebase develop onto your local branch. -* Push your fix to your fork. -* When ready to submit a pull request. +### Quick Start for general gitflow issues + +* Please fork and clone a local copy of [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs) +* Create a separate issue branch based off develop +* Commit your fix to the local branch +* Please update your local copy with the latest develop branch of [gitflow-cjs](https://github.com/CJ-Systems/gitflow-cjs) +* Rebase develop onto your local branch +* Push your fix to your fork +* Submit a pull request ### How to submit a pull request -For any new PRs releated to gitflow-cjs you can use on of the keywords from [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) to automatically close the releated issue. +For any new PRs related to gitflow-cjs you can use one of the keywords from [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) to automatically close the related issue. ## License terms @@ -217,6 +259,53 @@ and eventually finish it: git flow hotfix finish ``` +### Sync feature/bugfix branches (LBEM Edition) + +To synchronize your feature branch with the latest changes from the develop branch, use: +```shell +git flow feature sync [] +``` + +This command (inspired by [git-town sync](https://www.git-town.com/commands/sync.html)): +- Stashes any uncommitted changes +- Fetches from the remote +- Rebases (or merges, depending on config) your branch onto the latest develop +- Restores your stashed changes + +You can configure the sync strategy: +```shell +git flow config set syncstrategy rebase # default +git flow config set syncstrategy merge +``` + +### Create pull requests (LBEM Edition) + +Instead of merging locally with `finish`, you can create a pull request: +```shell +git flow feature propose [] +``` + +This will: +- Push your branch to the remote (if not already pushed) +- Create a pull request using `gh` CLI (if available) +- Fall back to opening the PR URL in your browser + +You can also configure `finish` to always create a PR instead of merging: +```shell +git flow config set finishmode propose # always create PR +git flow config set finishmode classic # traditional merge (default) +git flow config set finishmode ask # prompt each time +``` + +### Export/Import configuration (LBEM Edition) + +To share gitflow configuration with your team, export settings to a `.gitflow` file: +```shell +git flow config export +``` + +This creates a `.gitflow` file in your repository root that can be committed. When another developer clones the repo and runs `git flow init`, the settings are automatically imported. + ### Using Hooks and Filters diff --git a/git-flow b/git-flow index dfe85e4..d46e4ef 100755 --- a/git-flow +++ b/git-flow @@ -10,6 +10,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -102,6 +103,7 @@ usage() { echo " version Shows version information." echo " config Manage your git-flow configuration." echo " log Show log deviating from base branch." + echo " sync Sync current branch with its base (shortcut)." echo echo "Try 'git flow help' for details." } @@ -120,15 +122,34 @@ main() { . "$GITFLOW_DIR/gitflow-common" # allow user to request git action logging - DEFINE_boolean 'showcommands' false 'Show actions taken (git commands)' + DEFINE_boolean 'showcommands' false 'Show actions taken (git commands)' s # but if the user prefers that the logging is always on, # use the environmental variables. gitflow_override_flag_boolean 'showcommands' 'showcommands' # Sanity checks SUBCOMMAND="$1"; shift - if [ "${SUBCOMMAND}" = "finish" ] || [ "${SUBCOMMAND}" = "delete" ] || [ "${SUBCOMMAND}" = "publish" ] || [ "${SUBCOMMAND}" = "rebase" ]; then + if [ "${SUBCOMMAND}" = "finish" ] || [ "${SUBCOMMAND}" = "delete" ] || [ "${SUBCOMMAND}" = "publish" ] || [ "${SUBCOMMAND}" = "rebase" ] || [ "${SUBCOMMAND}" = "sync" ]; then _current_branch=$(git_current_branch) + + # Handle sync on develop/main branches specially + if [ "${SUBCOMMAND}" = "sync" ]; then + _develop_branch=$(git config --get gitflow.branch.develop 2>/dev/null) + _master_branch=$(git config --get gitflow.branch.master 2>/dev/null) + if [ "${_current_branch}" = "${_develop_branch}" ] || [ "${_current_branch}" = "${_master_branch}" ]; then + # For develop/main, just do a pull + echo "Syncing branch '${_current_branch}'..." + git fetch -q origin || { echo "Could not fetch from origin."; exit 1; } + if git rev-parse --verify "origin/${_current_branch}" >/dev/null 2>&1; then + git merge --ff-only "origin/${_current_branch}" || { echo "Could not fast-forward '${_current_branch}'. You may need to merge manually."; exit 1; } + echo "Branch '${_current_branch}' is now up to date with 'origin/${_current_branch}'." + else + echo "No remote tracking branch for '${_current_branch}'." + fi + exit 0 + fi + fi + if gitflow_is_prefixed_branch "${_current_branch}"; then if startswith "${_current_branch}" $(git config --get gitflow.prefix.feature); then SUBACTION="${SUBCOMMAND}" diff --git a/git-flow-bugfix b/git-flow-bugfix index 297324c..ce1b6c5 100644 --- a/git-flow-bugfix +++ b/git-flow-bugfix @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -59,6 +60,9 @@ git flow bugfix checkout git flow bugfix pull git flow bugfix delete git flow bugfix rename +git flow bugfix sync +git flow bugfix propose +git flow bugfix pr Manage your bugfix branches. @@ -261,14 +265,14 @@ no-ff! Never fast-forward during the merge DEFINE_boolean 'fetch' false "fetch from $ORIGIN before performing finish" F DEFINE_boolean 'rebase' false "rebase before merging" r DEFINE_boolean 'preserve-merges' false 'try to recreate merges while rebasing' p - DEFINE_boolean 'push' false "push to $ORIGIN after performing finish" + DEFINE_boolean 'push' false "push to $ORIGIN after performing finish" P DEFINE_boolean 'keep' false "keep branch after performing finish" k - DEFINE_boolean 'keepremote' false "keep the remote branch" - DEFINE_boolean 'keeplocal' false "keep the local branch" + DEFINE_boolean 'keepremote' false "keep the remote branch" 1 + DEFINE_boolean 'keeplocal' false "keep the local branch" 2 DEFINE_boolean 'force_delete' false "force delete bugfix branch after finish" D DEFINE_boolean 'squash' false "squash bugfix during merge" S - DEFINE_boolean 'squash-info' false "add branch info during squash" - DEFINE_boolean 'no-ff!' false "Don't fast-forward ever during merge " + DEFINE_boolean 'squash-info' false "add branch info during squash" 3 + DEFINE_boolean 'no-ff!' false "Don't fast-forward ever during merge " 4 # Override defaults with values from config gitflow_override_flag_boolean "bugfix.finish.fetch" "fetch" @@ -291,6 +295,38 @@ no-ff! Never fast-forward during the merge gitflow_use_current_branch_name fi + # LBEM Edition: Check finish mode (classic/propose/ask) + local finish_mode + finish_mode=$(gitflow_get_finish_mode "bugfix") + + case "$finish_mode" in + propose) + echo "Finish mode is 'propose'. Creating pull request instead of merging..." + cmd_propose "$@" + return $? + ;; + ask) + printf "Finish mode: [c]lassic merge, [p]ropose PR, [a]bort? " + read answer + case "$answer" in + p|P) + cmd_propose "$@" + return $? + ;; + a|A) + echo "Aborted." + exit 0 + ;; + c|C|*) + # Continue with classic finish + ;; + esac + ;; + classic|*) + # Continue with classic finish + ;; + esac + # Keeping both branches implies the --keep flag to be true. if flag keepremote && flag keeplocal; then FLAGS_keep=$FLAGS_TRUE @@ -303,6 +339,23 @@ no-ff! Never fast-forward during the merge BASE_BRANCH=${BASE_BRANCH:-$DEVELOP_BRANCH} git_local_branch_exists "$BASE_BRANCH" || die "The base '$BASE_BRANCH' doesn't exists locally or is not a branch. Can't finish the bugfix branch '$BRANCH'." + # LBEM Edition: Check if branch was already merged on GitHub/GitLab + # This handles the case where user did 'propose', merged on remote, and now runs 'finish' + if flag fetch; then + git_fetch_branch "$ORIGIN" "$BASE_BRANCH" + fi + if ! git_remote_branch_exists "$ORIGIN/$BRANCH"; then + # Remote branch doesn't exist - check if it was merged + if git_is_branch_merged_into "$BRANCH" "$BASE_BRANCH"; then + echo "Branch '$BRANCH' appears to have been merged and deleted on remote." + echo "Cleaning up local branch..." + run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" + gitflow_config_remove_base_branch "$BRANCH" + helper_finish_cleanup + exit 0 + fi + fi + # Detect if we're restoring from a merge conflict if [ -f "$DOT_GIT_DIR/.gitflow/MERGE_BASE" ]; then # @@ -842,3 +895,237 @@ showcommands! Show git commands while executing them " gitflow_rename_branch "$@" } + +# +# LBEM Edition: Sync command - synchronize bugfix branch with base branch +# +cmd_sync() { + OPTIONS_SPEC="\ +git flow bugfix sync [-h] [-p] [--prune] [] + +Synchronize bugfix branch with its base branch (like git-town sync). +This will: + 1. Stash any uncommitted changes + 2. Fetch updates from origin (with prune) + 3. Update the base branch + 4. Rebase or merge the bugfix branch on the updated base (configurable) + 5. Push the bugfix branch to origin (if -p flag or config) + 6. Optionally prune local branches that were deleted on remote + 7. Restore stashed changes + +When is omitted the current branch is used, but only if it's a bugfix branch. +-- +h,help! Show this help +showcommands! Show git commands while executing them +p,[no]push Push to origin after sync +[no]prune Prune local branches deleted on remote (default: prompt) +" + local did_stash sync_strategy prune_behavior + + # Define flags + DEFINE_boolean 'push' false "push to $ORIGIN after sync" p + DEFINE_boolean 'prune' true "prune local branches deleted on remote" + + # Override defaults with values from config + gitflow_override_flag_boolean "bugfix.sync.push" "push" + + # Parse arguments + parse_args "$@" + + # Get prune behavior from config + prune_behavior=$(git config --get gitflow.sync.prune 2>/dev/null) + [ -z "$prune_behavior" ] && prune_behavior="prompt" + + # Use current branch if no name is given + if [ "$NAME" = "" ]; then + gitflow_use_current_branch_name + fi + + # Sanity checks + require_branch "$BRANCH" + + BASE_BRANCH=$(gitflow_config_get_base_branch $BRANCH) + BASE_BRANCH=${BASE_BRANCH:-$DEVELOP_BRANCH} + + git_local_branch_exists "$BASE_BRANCH" || die "The base '$BASE_BRANCH' doesn't exist locally. Can't sync the bugfix branch '$BRANCH'." + + run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + # Step 1: Stash uncommitted changes + echo "Syncing bugfix branch '$BRANCH' with '$BASE_BRANCH'..." + gitflow_stash_save + did_stash=$? + + # Step 2: Fetch from origin (with prune) + echo "Fetching from '$ORIGIN'..." + git_do fetch -q --prune "$ORIGIN" || die "Could not fetch from '$ORIGIN'." + + # Step 3: Update base branch if it has a remote counterpart + if git_remote_branch_exists "$ORIGIN/$BASE_BRANCH"; then + echo "Updating base branch '$BASE_BRANCH'..." + git_do checkout -q "$BASE_BRANCH" || die "Could not check out '$BASE_BRANCH'." + git_do merge --ff-only "$ORIGIN/$BASE_BRANCH" 2>/dev/null || { + warn "Base branch '$BASE_BRANCH' has diverged from '$ORIGIN/$BASE_BRANCH'." + warn "Please resolve this manually before syncing." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Step 4: Get back to bugfix branch and sync with base + git_do checkout -q "$BRANCH" || die "Could not check out '$BRANCH'." + + # If remote bugfix branch exists, pull it first + if git_remote_branch_exists "$ORIGIN/$BRANCH"; then + echo "Pulling updates from '$ORIGIN/$BRANCH'..." + git_do pull --rebase -q "$ORIGIN" "$BRANCH" 2>/dev/null || { + warn "Could not pull from '$ORIGIN/$BRANCH'. You may need to resolve conflicts." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Rebase or merge based on strategy + sync_strategy=$(gitflow_get_sync_strategy "bugfix") + echo "Syncing with base branch using $sync_strategy strategy..." + + if [ "$sync_strategy" = "rebase" ]; then + git_do rebase "$BASE_BRANCH" || { + warn "Rebase failed. Please resolve conflicts and run 'git rebase --continue'." + warn "After resolving, run 'git flow bugfix sync' again." + gitflow_stash_pop $did_stash + exit 1 + } + else + git_do merge --no-edit "$BASE_BRANCH" || { + warn "Merge failed. Please resolve conflicts and commit." + warn "After resolving, run 'git flow bugfix sync' again." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Step 5: Push if requested + if flag push; then + echo "Pushing '$BRANCH' to '$ORIGIN'..." + if [ "$sync_strategy" = "rebase" ]; then + git_do push --force-with-lease "$ORIGIN" "$BRANCH" || warn "Could not push to '$ORIGIN'. You may need to push manually." + else + git_do push "$ORIGIN" "$BRANCH" || warn "Could not push to '$ORIGIN'. You may need to push manually." + fi + fi + + # Step 6: Prune stale local branches + if flag prune; then + gitflow_prune_branches "$prune_behavior" + fi + + # Step 7: Restore stashed changes + gitflow_stash_pop $did_stash + + run_post_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + echo + echo "Summary of actions:" + echo "- Bugfix branch '$BRANCH' was synced with '$BASE_BRANCH'" + if flag push; then + echo "- Bugfix branch '$BRANCH' was pushed to '$ORIGIN'" + fi + if flag prune; then + echo "- Stale remote-tracking branches were pruned" + fi + echo "- You are now on branch '$(git_current_branch)'" + echo +} + +# +# LBEM Edition: Propose command - create a pull/merge request +# +cmd_propose() { + OPTIONS_SPEC="\ +git flow bugfix propose [-h] [-d] [-a ] [-r ] [-l ] [] + +Create a pull request (or merge request) for bugfix branch . +This will: + 1. Push the bugfix branch to origin (if not already pushed) + 2. Create a PR using gh/glab CLI (if available) or open browser + +When is omitted the current branch is used, but only if it's a bugfix branch. +-- +h,help! Show this help +showcommands! Show git commands while executing them +d,[no]draft Create as draft PR +a,assignee= Assign PR to user (use @me for self) +r,reviewer= Request review from user +l,label= Add labels (comma-separated) +" + local _draft _assignee _reviewer _labels + + # Define flags + DEFINE_boolean 'draft' false 'create as draft PR' d + DEFINE_string 'assignee' '' 'assign PR to user' a + DEFINE_string 'reviewer' '' 'request review from user' r + DEFINE_string 'label' '' 'add labels (comma-separated)' l + + # Override defaults with values from config + gitflow_override_flag_boolean "bugfix.propose.draft" "draft" + gitflow_override_flag_string "bugfix.propose.assignee" "assignee" + gitflow_override_flag_string "bugfix.propose.reviewer" "reviewer" + gitflow_override_flag_string "bugfix.propose.labels" "label" + + # Parse arguments + parse_args "$@" + + # Use current branch if no name is given + if [ "$NAME" = "" ]; then + gitflow_use_current_branch_name + fi + + # Sanity checks + require_clean_working_tree + require_branch "$BRANCH" + + BASE_BRANCH=$(gitflow_config_get_base_branch $BRANCH) + BASE_BRANCH=${BASE_BRANCH:-$DEVELOP_BRANCH} + + run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + # Push branch to origin if not already there + if ! git_remote_branch_exists "$ORIGIN/$BRANCH"; then + echo "Publishing '$BRANCH' to '$ORIGIN'..." + git_do push -u "$ORIGIN" "$BRANCH:$BRANCH" || die "Could not push '$BRANCH' to '$ORIGIN'." + else + # Make sure local branch is up to date with remote + echo "Pushing any local changes to '$ORIGIN/$BRANCH'..." + git_do push "$ORIGIN" "$BRANCH" || die "Could not push '$BRANCH' to '$ORIGIN'." + fi + + # Convert flag values + if flag draft; then + _draft="true" + else + _draft="false" + fi + _assignee="$FLAGS_assignee" + _reviewer="$FLAGS_reviewer" + _labels="$FLAGS_label" + + # Create PR with options + gitflow_create_pr "$BRANCH" "$BASE_BRANCH" "bugfix" "$_draft" "$_assignee" "$_reviewer" "$_labels" + + run_post_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + echo + echo "Summary of actions:" + echo "- Bugfix branch '$BRANCH' was pushed to '$ORIGIN'" + echo "- A pull request was created (or browser opened) targeting '$BASE_BRANCH'" + echo "- You are now on branch '$(git_current_branch)'" + echo +} + +# +# LBEM Edition: PR alias for propose +# +cmd_pr() { + cmd_propose "$@" +} \ No newline at end of file diff --git a/git-flow-config b/git-flow-config index 70843c9..b9e6223 100644 --- a/git-flow-config +++ b/git-flow-config @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -48,6 +49,7 @@ usage() { git flow config [list] git flow config set git flow config base +git flow config export Manage the git-flow configuration. @@ -131,6 +133,16 @@ file= Use given config file output=$(git config $gitflow_config_option --get gitflow.prefix.versiontag) echo "Version tag prefix: $output " + + # LBEM Edition: Additional config options + output=$(git config $gitflow_config_option --get gitflow.finish.mode) + echo "Finish mode (classic/propose/ask): ${output:-classic} " + + output=$(git config $gitflow_config_option --get gitflow.sync.strategy) + echo "Sync strategy (rebase/merge): ${output:-rebase} " + + output=$(git config $gitflow_config_option --get gitflow.propose.open) + echo "Auto-open browser on propose: ${output:-true} " } cmd_set() { @@ -195,6 +207,18 @@ file= Use given config file cfg_option="gitflow.multi-hotfix" txt="Allow multiple hotfix branches" ;; + finishmode) + cfg_option="gitflow.finish.mode" + txt="Default finish mode (classic/propose/ask)" + ;; + syncstrategy) + cfg_option="gitflow.sync.strategy" + txt="Sync strategy (rebase/merge)" + ;; + proposeopen) + cfg_option="gitflow.propose.open" + txt="Auto-open browser on propose" + ;; *) die_help "Invalid option given." ;; @@ -239,6 +263,30 @@ file= Use given config file esac fi + # LBEM Edition: Validate finish mode + if [ $OPTION = "finishmode" ]; then + gitflow_check_finish_mode "${value}" || \ + die "Invalid value for option 'finishmode'. Valid values are 'classic', 'propose', or 'ask'" + fi + + # LBEM Edition: Validate sync strategy + if [ $OPTION = "syncstrategy" ]; then + gitflow_check_sync_strategy "${value}" || \ + die "Invalid value for option 'syncstrategy'. Valid values are 'rebase' or 'merge'" + fi + + # LBEM Edition: Validate propose open + if [ $OPTION = "proposeopen" ]; then + check_boolean "${value}" + case $? in + ${FLAGS_ERROR}) + die "Invalid value for option 'proposeopen'. Valid values are 'true' or 'false'" + ;; + *) + ;; + esac + fi + git_do config $gitflow_config_option $cfg_option "$value" case $? in @@ -304,6 +352,61 @@ cmd_help() { exit 0 } +# +# LBEM Edition: Export command - write current config to .gitflow file +# +cmd_export() { + OPTIONS_SPEC="\ +git flow config export + +Export the current git-flow configuration to a .gitflow file in the repository root. +This file can be committed to share settings with your team. +-- +h,help! Show this help +" + local dotgitflow_file + + FLAGS "$@" || exit $? + + dotgitflow_file="$GIT_CURRENT_REPO_DIR/.gitflow" + + echo "Exporting git-flow configuration to '$dotgitflow_file'..." + + # Create or overwrite the .gitflow file + cat > "$dotgitflow_file" << EOF +# git-flow configuration file +# This file can be committed to share git-flow settings with your team. +# Generated by git-flow LBEM Edition + +[branch] + master = $(git config --get gitflow.branch.master) + develop = $(git config --get gitflow.branch.develop) + +[prefix] + feature = $(git config --get gitflow.prefix.feature) + bugfix = $(git config --get gitflow.prefix.bugfix) + release = $(git config --get gitflow.prefix.release) + hotfix = $(git config --get gitflow.prefix.hotfix) + support = $(git config --get gitflow.prefix.support) + versiontag = $(git config --get gitflow.prefix.versiontag) + +[finish] + mode = $(git config --get gitflow.finish.mode || echo classic) + +[sync] + strategy = $(git config --get gitflow.sync.strategy || echo rebase) + +[propose] + open = $(git config --get gitflow.propose.open || echo true) +EOF + + echo + echo "Summary of actions:" + echo "- Configuration exported to '$dotgitflow_file'" + echo "- You can commit this file to share settings with your team" + echo +} + # Private functions __set_base () { diff --git a/git-flow-feature b/git-flow-feature index c5c5eea..3046cc8 100644 --- a/git-flow-feature +++ b/git-flow-feature @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -60,6 +61,9 @@ git flow feature pull git flow feature delete git flow feature rename git flow feature release +git flow feature sync +git flow feature propose +git flow feature pr Manage your feature branches. @@ -262,14 +266,14 @@ no-ff! Never fast-forward during the merge DEFINE_boolean 'fetch' false "fetch from $ORIGIN before performing finish" F DEFINE_boolean 'rebase' false "rebase before merging" r DEFINE_boolean 'preserve-merges' false 'try to recreate merges while rebasing' p - DEFINE_boolean 'push' false "push to $ORIGIN after performing finish" + DEFINE_boolean 'push' false "push to $ORIGIN after performing finish" P DEFINE_boolean 'keep' false "keep branch after performing finish" k - DEFINE_boolean 'keepremote' false "keep the remote branch" - DEFINE_boolean 'keeplocal' false "keep the local branch" + DEFINE_boolean 'keepremote' false "keep the remote branch" 1 + DEFINE_boolean 'keeplocal' false "keep the local branch" 2 DEFINE_boolean 'force_delete' false "force delete feature branch after finish" D DEFINE_boolean 'squash' false "squash feature during merge" S - DEFINE_boolean 'squash-info' false "add branch info during squash" - DEFINE_boolean 'no-ff!' false "Don't fast-forward ever during merge " + DEFINE_boolean 'squash-info' false "add branch info during squash" 3 + DEFINE_boolean 'no-ff!' false "Don't fast-forward ever during merge " 4 # Override defaults with values from config gitflow_override_flag_boolean "feature.finish.fetch" "fetch" @@ -292,6 +296,38 @@ no-ff! Never fast-forward during the merge gitflow_use_current_branch_name fi + # LBEM Edition: Check finish mode (classic/propose/ask) + local finish_mode + finish_mode=$(gitflow_get_finish_mode "feature") + + case "$finish_mode" in + propose) + echo "Finish mode is 'propose'. Creating pull request instead of merging..." + cmd_propose "$@" + return $? + ;; + ask) + printf "Finish mode: [c]lassic merge, [p]ropose PR, [a]bort? " + read answer + case "$answer" in + p|P) + cmd_propose "$@" + return $? + ;; + a|A) + echo "Aborted." + exit 0 + ;; + c|C|*) + # Continue with classic finish + ;; + esac + ;; + classic|*) + # Continue with classic finish + ;; + esac + # Keeping both branches implies the --keep flag to be true. if flag keepremote && flag keeplocal; then FLAGS_keep=$FLAGS_TRUE @@ -304,6 +340,23 @@ no-ff! Never fast-forward during the merge BASE_BRANCH=${BASE_BRANCH:-$DEVELOP_BRANCH} git_local_branch_exists "$BASE_BRANCH" || die "The base '$BASE_BRANCH' doesn't exists locally or is not a branch. Can't finish the feature branch '$BRANCH'." + # LBEM Edition: Check if branch was already merged on GitHub/GitLab + # This handles the case where user did 'propose', merged on remote, and now runs 'finish' + if flag fetch; then + git_fetch_branch "$ORIGIN" "$BASE_BRANCH" + fi + if ! git_remote_branch_exists "$ORIGIN/$BRANCH"; then + # Remote branch doesn't exist - check if it was merged + if git_is_branch_merged_into "$BRANCH" "$BASE_BRANCH"; then + echo "Branch '$BRANCH' appears to have been merged and deleted on remote." + echo "Cleaning up local branch..." + run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" + gitflow_config_remove_base_branch "$BRANCH" + helper_finish_cleanup + exit 0 + fi + fi + # Detect if we're restoring from a merge conflict if [ -f "$DOT_GIT_DIR/.gitflow/MERGE_BASE" ]; then # @@ -874,3 +927,237 @@ showcommands! Show git commands while executing them " gitflow_rename_branch "$@" } + +# +# LBEM Edition: Sync command - synchronize feature branch with base branch +# +cmd_sync() { + OPTIONS_SPEC="\ +git flow feature sync [-h] [-p] [--prune] [] + +Synchronize feature branch with its base branch (like git-town sync). +This will: + 1. Stash any uncommitted changes + 2. Fetch updates from origin (with prune) + 3. Update the base branch + 4. Rebase or merge the feature branch on the updated base (configurable) + 5. Push the feature branch to origin (if -p flag or config) + 6. Optionally prune local branches that were deleted on remote + 7. Restore stashed changes + +When is omitted the current branch is used, but only if it's a feature branch. +-- +h,help! Show this help +showcommands! Show git commands while executing them +p,[no]push Push to origin after sync +[no]prune Prune local branches deleted on remote (default: prompt) +" + local did_stash sync_strategy prune_behavior + + # Define flags + DEFINE_boolean 'push' false "push to $ORIGIN after sync" p + DEFINE_boolean 'prune' true "prune local branches deleted on remote" + + # Override defaults with values from config + gitflow_override_flag_boolean "feature.sync.push" "push" + + # Parse arguments + parse_args "$@" + + # Get prune behavior from config + prune_behavior=$(git config --get gitflow.sync.prune 2>/dev/null) + [ -z "$prune_behavior" ] && prune_behavior="prompt" + + # Use current branch if no name is given + if [ "$NAME" = "" ]; then + gitflow_use_current_branch_name + fi + + # Sanity checks + require_branch "$BRANCH" + + BASE_BRANCH=$(gitflow_config_get_base_branch $BRANCH) + BASE_BRANCH=${BASE_BRANCH:-$DEVELOP_BRANCH} + + git_local_branch_exists "$BASE_BRANCH" || die "The base '$BASE_BRANCH' doesn't exist locally. Can't sync the feature branch '$BRANCH'." + + run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + # Step 1: Stash uncommitted changes + echo "Syncing feature branch '$BRANCH' with '$BASE_BRANCH'..." + gitflow_stash_save + did_stash=$? + + # Step 2: Fetch from origin (with prune) + echo "Fetching from '$ORIGIN'..." + git_do fetch -q --prune "$ORIGIN" || die "Could not fetch from '$ORIGIN'." + + # Step 3: Update base branch if it has a remote counterpart + if git_remote_branch_exists "$ORIGIN/$BASE_BRANCH"; then + echo "Updating base branch '$BASE_BRANCH'..." + git_do checkout -q "$BASE_BRANCH" || die "Could not check out '$BASE_BRANCH'." + git_do merge --ff-only "$ORIGIN/$BASE_BRANCH" 2>/dev/null || { + warn "Base branch '$BASE_BRANCH' has diverged from '$ORIGIN/$BASE_BRANCH'." + warn "Please resolve this manually before syncing." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Step 4: Get back to feature branch and sync with base + git_do checkout -q "$BRANCH" || die "Could not check out '$BRANCH'." + + # If remote feature branch exists, pull it first + if git_remote_branch_exists "$ORIGIN/$BRANCH"; then + echo "Pulling updates from '$ORIGIN/$BRANCH'..." + git_do pull --rebase -q "$ORIGIN" "$BRANCH" 2>/dev/null || { + warn "Could not pull from '$ORIGIN/$BRANCH'. You may need to resolve conflicts." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Rebase or merge based on strategy + sync_strategy=$(gitflow_get_sync_strategy "feature") + echo "Syncing with base branch using $sync_strategy strategy..." + + if [ "$sync_strategy" = "rebase" ]; then + git_do rebase "$BASE_BRANCH" || { + warn "Rebase failed. Please resolve conflicts and run 'git rebase --continue'." + warn "After resolving, run 'git flow feature sync' again." + gitflow_stash_pop $did_stash + exit 1 + } + else + git_do merge --no-edit "$BASE_BRANCH" || { + warn "Merge failed. Please resolve conflicts and commit." + warn "After resolving, run 'git flow feature sync' again." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Step 5: Push if requested + if flag push; then + echo "Pushing '$BRANCH' to '$ORIGIN'..." + if [ "$sync_strategy" = "rebase" ]; then + git_do push --force-with-lease "$ORIGIN" "$BRANCH" || warn "Could not push to '$ORIGIN'. You may need to push manually." + else + git_do push "$ORIGIN" "$BRANCH" || warn "Could not push to '$ORIGIN'. You may need to push manually." + fi + fi + + # Step 6: Prune stale local branches + if flag prune; then + gitflow_prune_branches "$prune_behavior" + fi + + # Step 7: Restore stashed changes + gitflow_stash_pop $did_stash + + run_post_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + echo + echo "Summary of actions:" + echo "- Feature branch '$BRANCH' was synced with '$BASE_BRANCH'" + if flag push; then + echo "- Feature branch '$BRANCH' was pushed to '$ORIGIN'" + fi + if flag prune; then + echo "- Stale remote-tracking branches were pruned" + fi + echo "- You are now on branch '$(git_current_branch)'" + echo +} + +# +# LBEM Edition: Propose command - create a pull/merge request +# +cmd_propose() { + OPTIONS_SPEC="\ +git flow feature propose [-h] [-d] [-a ] [-r ] [-l ] [] + +Create a pull request (or merge request) for feature branch . +This will: + 1. Push the feature branch to origin (if not already pushed) + 2. Create a PR using gh/glab CLI (if available) or open browser + +When is omitted the current branch is used, but only if it's a feature branch. +-- +h,help! Show this help +showcommands! Show git commands while executing them +d,[no]draft Create as draft PR +a,assignee= Assign PR to user (use @me for self) +r,reviewer= Request review from user +l,label= Add labels (comma-separated) +" + local _draft _assignee _reviewer _labels + + # Define flags + DEFINE_boolean 'draft' false 'create as draft PR' d + DEFINE_string 'assignee' '' 'assign PR to user' a + DEFINE_string 'reviewer' '' 'request review from user' r + DEFINE_string 'label' '' 'add labels (comma-separated)' l + + # Override defaults with values from config + gitflow_override_flag_boolean "feature.propose.draft" "draft" + gitflow_override_flag_string "feature.propose.assignee" "assignee" + gitflow_override_flag_string "feature.propose.reviewer" "reviewer" + gitflow_override_flag_string "feature.propose.labels" "label" + + # Parse arguments + parse_args "$@" + + # Use current branch if no name is given + if [ "$NAME" = "" ]; then + gitflow_use_current_branch_name + fi + + # Sanity checks + require_clean_working_tree + require_branch "$BRANCH" + + BASE_BRANCH=$(gitflow_config_get_base_branch $BRANCH) + BASE_BRANCH=${BASE_BRANCH:-$DEVELOP_BRANCH} + + run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + # Push branch to origin if not already there + if ! git_remote_branch_exists "$ORIGIN/$BRANCH"; then + echo "Publishing '$BRANCH' to '$ORIGIN'..." + git_do push -u "$ORIGIN" "$BRANCH:$BRANCH" || die "Could not push '$BRANCH' to '$ORIGIN'." + else + # Make sure local branch is up to date with remote + echo "Pushing any local changes to '$ORIGIN/$BRANCH'..." + git_do push "$ORIGIN" "$BRANCH" || die "Could not push '$BRANCH' to '$ORIGIN'." + fi + + # Convert flag values + if flag draft; then + _draft="true" + else + _draft="false" + fi + _assignee="$FLAGS_assignee" + _reviewer="$FLAGS_reviewer" + _labels="$FLAGS_label" + + # Create PR with options + gitflow_create_pr "$BRANCH" "$BASE_BRANCH" "feature" "$_draft" "$_assignee" "$_reviewer" "$_labels" + + run_post_hook "$NAME" "$ORIGIN" "$BRANCH" "$BASE_BRANCH" + + echo + echo "Summary of actions:" + echo "- Feature branch '$BRANCH' was pushed to '$ORIGIN'" + echo "- A pull request was created (or browser opened) targeting '$BASE_BRANCH'" + echo "- You are now on branch '$(git_current_branch)'" + echo +} + +# +# LBEM Edition: PR alias for propose +# +cmd_pr() { + cmd_propose "$@" +} \ No newline at end of file diff --git a/git-flow-hotfix b/git-flow-hotfix index 8d3c649..0cbbba3 100644 --- a/git-flow-hotfix +++ b/git-flow-hotfix @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -53,6 +54,7 @@ git flow hotfix [list] git flow hotfix start git flow hotfix finish git flow hotfix publish +git flow hotfix sync git flow hotfix delete git flow hotfix rebase git flow hotfix track @@ -389,8 +391,9 @@ S,[no]squash Squash hotfix during merge T,tagname! Use given tag name c,cherrypick Cherry Pick to $DEVELOP_BRANCH instead of merge nodevelopmerge! Don't back-merge develop branch +9,[no]create-release Create a GitHub/GitLab release for the tag " - local opts commit keepmsg remotebranchdeleted localbranchdeleted + local opts commit keepmsg remotebranchdeleted localbranchdeleted release_created # Define flags DEFINE_boolean 'fetch' false "fetch from $ORIGIN before performing finish" F @@ -411,6 +414,7 @@ nodevelopmerge! Don't back-merge develop branch DEFINE_string 'tagname' "" "use the given tag name" T DEFINE_boolean 'cherrypick' false "Cherry Pick to $DEVELOP_BRANCH instead of merge" c DEFINE_boolean 'nodevelopmerge' false "don't merge $BRANCH into $DEVELOP_BRANCH " + DEFINE_boolean 'create-release' false "create a GitHub/GitLab release for the tag" 9 # Override defaults with values from config gitflow_override_flag_boolean "hotfix.finish.fetch" "fetch" @@ -430,6 +434,7 @@ nodevelopmerge! Don't back-merge develop branch gitflow_override_flag_string "hotfix.finish.messagefile" "messagefile" gitflow_override_flag_boolean "hotfix.finish.cherrypick" "cherrypick" gitflow_override_flag_boolean "hotfix.finish.nodevelopmerge" "nodevelopmerge" + gitflow_override_flag_boolean "hotfix.finish.create-release" "create-release" # Parse arguments parse_args "$@" @@ -448,6 +453,7 @@ nodevelopmerge! Don't back-merge develop branch remotebranchdeleted=$FLAGS_FALSE localbranchdeleted=$FLAGS_FALSE + release_created=$FLAGS_FALSE # Handle flags that imply other flags if [ "$FLAGS_signingkey" != "" ]; then @@ -697,6 +703,12 @@ if flag cherrypick; then fi fi + # Create GitHub/GitLab release if requested + if noflag notag && flag create-release; then + gitflow_create_release "$VERSION_PREFIX$TAGNAME" "$VERSION_PREFIX$TAGNAME" + release_created=$FLAGS_TRUE + fi + # Delete branch if noflag keep; then @@ -769,6 +781,9 @@ if flag cherrypick; then echo "- '$BASE_BRANCH' and tags have been pushed to '$ORIGIN'" fi fi + if [ $release_created -eq $FLAGS_TRUE ]; then + echo "- A GitHub/GitLab release was created for '$VERSION_PREFIX$TAGNAME'" + fi echo "- You are now on branch '$(git_current_branch)'" echo @@ -839,6 +854,86 @@ r,[no]remote Delete remote branch echo } +# +# LBEM Edition: Sync command - synchronize hotfix branch with remote +# +cmd_sync() { + OPTIONS_SPEC="\ +git flow hotfix sync [-h] [-p] [] + +Synchronize hotfix branch with its remote counterpart. +This will: + 1. Stash any uncommitted changes + 2. Pull updates from the remote hotfix branch + 3. Push the hotfix branch to origin (if -p flag or config) + 4. Restore stashed changes + +When is omitted the current branch is used, but only if it's a hotfix branch. +-- +h,help! Show this help +showcommands! Show git commands while executing them +p,[no]push Push to origin after sync +" + local did_stash + + # Define flags + DEFINE_boolean 'push' false "push to $ORIGIN after sync" p + + # Override defaults with values from config + gitflow_override_flag_boolean "hotfix.sync.push" "push" + + # Parse arguments + parse_args "$@" + + # Use current branch if no name is given + if [ "$VERSION" = "" ]; then + gitflow_use_current_branch_version + fi + + # Sanity checks + require_branch "$BRANCH" + + run_pre_hook "$VERSION" "$ORIGIN" "$BRANCH" + + # Step 1: Stash uncommitted changes + echo "Syncing hotfix branch '$BRANCH'..." + gitflow_stash_save + did_stash=$? + + # Step 2: Fetch and pull from remote hotfix branch + echo "Fetching from '$ORIGIN'..." + git_do fetch -q "$ORIGIN" || die "Could not fetch from '$ORIGIN'." + + if git_remote_branch_exists "$ORIGIN/$BRANCH"; then + echo "Pulling updates from '$ORIGIN/$BRANCH'..." + git_do pull --rebase -q "$ORIGIN" "$BRANCH" 2>/dev/null || { + warn "Could not pull from '$ORIGIN/$BRANCH'. You may need to resolve conflicts." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Step 3: Push if requested + if flag push; then + echo "Pushing '$BRANCH' to '$ORIGIN'..." + git_do push "$ORIGIN" "$BRANCH" || warn "Could not push to '$ORIGIN'. You may need to push manually." + fi + + # Step 4: Restore stashed changes + gitflow_stash_pop $did_stash + + run_post_hook "$VERSION" "$ORIGIN" "$BRANCH" + + echo + echo "Summary of actions:" + echo "- Hotfix branch '$BRANCH' was synced with '$ORIGIN/$BRANCH'" + if flag push; then + echo "- Hotfix branch '$BRANCH' was pushed to '$ORIGIN'" + fi + echo "- You are now on branch '$(git_current_branch)'" + echo +} + cmd_rename() { OPTIONS_SPEC="\ git flow hotfix rename [] diff --git a/git-flow-init b/git-flow-init index 8a4d767..27dbd3e 100644 --- a/git-flow-init +++ b/git-flow-init @@ -64,7 +64,7 @@ git flow init [-h] [-d] [-f] [-g] Setup a git repository for git flow usage. Can also be used to start a git repository. -- h,help! Show this help -showcommands! Show git commands while executing them +v,showcommands! Show git commands while executing them d,[no]defaults Use default branch naming conventions f,[no]force Force setting of gitflow branches, even if already configured g,[no]sign Sign initial commit when creating a repository @@ -73,14 +73,14 @@ p,feature! Feature branches b,bugfix! Bugfix branches r,release! Release branches x,hotfix! Hotfix branches -s,support! Support branches +u,support! Support branches t,tag! Version tag prefix Use config file location -local! use repository config file -global! use global config file -system! use system config file -file= use given config file +l,local! use repository config file +o,global! use global config file +y,system! use system config file +c:,file= use given config file " local gitflow_config_option should_check_existence branchcount guess local master_branch develop_branch default_suggestion answer prefix @@ -88,16 +88,16 @@ file= use given config file # Define flags DEFINE_boolean 'force' false 'force setting of gitflow branches, even if already configured' f DEFINE_boolean 'defaults' false 'use default branch naming conventions' d - DEFINE_boolean 'local' false 'use repository config file' - DEFINE_boolean 'global' false 'use global config file' - DEFINE_boolean 'system' false 'use system config file' + DEFINE_boolean 'local' false 'use repository config file' l + DEFINE_boolean 'global' false 'use global config file' o + DEFINE_boolean 'system' false 'use system config file' y DEFINE_boolean 'sign' false 'sign initial commit when creating a repository' g - DEFINE_string 'file' "" 'use given config file' + DEFINE_string 'file' "" 'use given config file' c DEFINE_string 'feature' "" 'feature branches' p DEFINE_string 'bugfix' "" 'bugfix branches' b DEFINE_string 'release' "" 'release branches' r DEFINE_string 'hotfix' "" 'hotfix branches' x - DEFINE_string 'support' "" 'support branches' s + DEFINE_string 'support' "" 'support branches' u DEFINE_string 'tag' "" 'version tag prefix' t # Override defaults with values from config @@ -429,7 +429,7 @@ file= use given config file git_do config $gitflow_config_option gitflow.path.hooks "$hooks_dir" fi - # Automate pre-commit hook install if it exists on git-flow hooks directory + # Automate pre-commit hook install if it exists on git-flow hooks directory hooks_dir=$(git config --get gitflow.path.hooks) hooks_dir=${hooks_dir%%/} if [ -f "$hooks_dir/pre-commit" ]; then @@ -451,6 +451,98 @@ file= use given config file fi fi + # LBEM Edition settings + if flag force || \ + ! git config --get gitflow.feature.finish.mode >/dev/null 2>&1; then + echo + echo "LBEM Edition settings:" + fi + + # Feature finish mode (merge or rebase) + if ! git config --get gitflow.feature.finish.mode >/dev/null 2>&1 || flag force; then + default_suggestion=$(git config --get gitflow.feature.finish.mode || echo "merge") + printf "Feature finish mode (merge/rebase)? [$default_suggestion] " + if noflag defaults; then + read answer + else + printf "\n" + fi + answer=${answer:-$default_suggestion} + if [ "$answer" = "merge" ] || [ "$answer" = "rebase" ]; then + git_do config $gitflow_config_option gitflow.feature.finish.mode "$answer" + else + warn "Invalid mode '$answer'. Using default 'merge'." + git_do config $gitflow_config_option gitflow.feature.finish.mode "merge" + fi + fi + + # Default draft PR setting + if ! git config --get gitflow.propose.draft >/dev/null 2>&1 || flag force; then + default_suggestion=$(git config --bool --get gitflow.propose.draft 2>/dev/null) + [ -z "$default_suggestion" ] && default_suggestion="false" + printf "Create draft PRs by default? (true/false) [$default_suggestion] " + if noflag defaults; then + read answer + else + printf "\n" + fi + answer=${answer:-$default_suggestion} + if [ "$answer" = "true" ] || [ "$answer" = "false" ]; then + git_do config $gitflow_config_option gitflow.propose.draft "$answer" + fi + fi + + # Auto-label PRs with branch type + if ! git config --get gitflow.propose.autolabel >/dev/null 2>&1 || flag force; then + default_suggestion=$(git config --bool --get gitflow.propose.autolabel 2>/dev/null) + [ -z "$default_suggestion" ] && default_suggestion="false" + printf "Auto-label PRs with branch type (feature/bugfix)? (true/false) [$default_suggestion] " + if noflag defaults; then + read answer + else + printf "\n" + fi + answer=${answer:-$default_suggestion} + if [ "$answer" = "true" ] || [ "$answer" = "false" ]; then + git_do config $gitflow_config_option gitflow.propose.autolabel "$answer" + fi + fi + + # Create GitHub/GitLab releases on release finish + if ! git config --get gitflow.release.finish.create-release >/dev/null 2>&1 || flag force; then + default_suggestion=$(git config --bool --get gitflow.release.finish.create-release 2>/dev/null) + [ -z "$default_suggestion" ] && default_suggestion="false" + printf "Create GitHub/GitLab releases on release finish? (true/false) [$default_suggestion] " + if noflag defaults; then + read answer + else + printf "\n" + fi + answer=${answer:-$default_suggestion} + if [ "$answer" = "true" ] || [ "$answer" = "false" ]; then + git_do config $gitflow_config_option gitflow.release.finish.create-release "$answer" + fi + fi + + # Sync prune behavior + if ! git config --get gitflow.sync.prune >/dev/null 2>&1 || flag force; then + default_suggestion=$(git config --get gitflow.sync.prune 2>/dev/null) + [ -z "$default_suggestion" ] && default_suggestion="prompt" + printf "Sync prune behavior (prompt/delete/keep)? [$default_suggestion] " + if noflag defaults; then + read answer + else + printf "\n" + fi + answer=${answer:-$default_suggestion} + if [ "$answer" = "prompt" ] || [ "$answer" = "delete" ] || [ "$answer" = "keep" ]; then + git_do config $gitflow_config_option gitflow.sync.prune "$answer" + else + warn "Invalid prune behavior '$answer'. Using default 'prompt'." + git_do config $gitflow_config_option gitflow.sync.prune "prompt" + fi + fi + # TODO: what to do with origin? } diff --git a/git-flow-release b/git-flow-release index 3acdfd3..6e32da9 100644 --- a/git-flow-release +++ b/git-flow-release @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -43,10 +44,11 @@ # Called when the base of the release is the $DEVELOP_BRANCH # _finish_from_develop() { - local opts merge_branch commit keepmsg remotebranchdeleted localbranchdeleted compare_refs_result merge_result + local opts merge_branch commit keepmsg remotebranchdeleted localbranchdeleted compare_refs_result merge_result release_created remotebranchdeleted=$FLAGS_FALSE localbranchdeleted=$FLAGS_FALSE + release_created=$FLAGS_FALSE # Update local branches with remote branches if flag fetch; then @@ -198,6 +200,12 @@ _finish_from_develop() { fi fi + # Create GitHub/GitLab release if requested + if noflag notag && flag create-release; then + gitflow_create_release "$VERSION_PREFIX$TAGNAME" "$VERSION_PREFIX$TAGNAME" + release_created=$FLAGS_TRUE + fi + # Delete branch if noflag keep; then @@ -266,6 +274,9 @@ _finish_from_develop() { if flag push; then echo "- '$DEVELOP_BRANCH', '$MASTER_BRANCH' and tags have been pushed to '$ORIGIN'" fi + if [ $release_created -eq $FLAGS_TRUE ]; then + echo "- A GitHub/GitLab release was created for '$VERSION_PREFIX$TAGNAME'" + fi echo "- You are now on branch '$(git_current_branch)'" echo } @@ -276,10 +287,11 @@ _finish_from_develop() { # _finish_base() { - local opts merge_branch commit keepmsg localbranchdeleted remotebranchdeleted + local opts merge_branch commit keepmsg localbranchdeleted remotebranchdeleted release_created remotebranchdeleted=$FLAGS_FALSE localbranchdeleted=$FLAGS_FALSE + release_created=$FLAGS_FALSE # Update local branches with remote branches if flag fetch; then @@ -386,6 +398,12 @@ _finish_base() { fi fi + # Create GitHub/GitLab release if requested + if noflag notag && flag create-release; then + gitflow_create_release "$VERSION_PREFIX$TAGNAME" "$VERSION_PREFIX$TAGNAME" + release_created=$FLAGS_TRUE + fi + echo echo "Summary of actions:" if flag fetch; then @@ -418,6 +436,9 @@ _finish_base() { if flag push; then echo "- '$BASE_BRANCH' and tags have been pushed to '$ORIGIN'" fi + if [ $release_created -eq $FLAGS_TRUE ]; then + echo "- A GitHub/GitLab release was created for '$VERSION_PREFIX$TAGNAME'" + fi echo "- You are now on branch '$(git_current_branch)'" echo } @@ -437,6 +458,7 @@ git flow release start git flow release finish git flow release branch git flow release publish +git flow release sync git flow release track git flow release rebase git flow release delete @@ -564,20 +586,21 @@ u,signingkey! Use the given GPG-key for the digital signature (implies -s) m,message! Use the given tag message f,[no]messagefile= Use the contents of the given file as a tag message p,[no]push Push to origin after performing finish -[no]pushproduction Push the production branch -[no]pushdevelop Push the develop branch -[no]pushtag Push the tag +1,[no]pushproduction Push the production branch +2,[no]pushdevelop Push the develop branch +3,[no]pushtag Push the tag k,[no]keep Keep branch after performing finish -[no]keepremote Keep the remote branch -[no]keeplocal Keep the local branch +4,[no]keepremote Keep the remote branch +5,[no]keeplocal Keep the local branch D,[no]force_delete Force delete release branch after finish n,[no]tag Don't tag this release b,[no]nobackmerge Don't back-merge master, or tag if applicable, in develop S,[no]squash Squash release during merge -[no]ff-master Fast forward master branch if possible +6,[no]squash-info Add branch info during squash +7,[no]ff-master Fast forward master branch if possible e,[no]edit The --noedit option can be used to accept the auto-generated message on merging T,tagname! Use given tag name -nodevelopmerge! Don't back-merge develop branch +8,nodevelopmerge! Don't back-merge develop branch " local base @@ -589,21 +612,21 @@ nodevelopmerge! Don't back-merge develop branch DEFINE_string 'message' "" "use the given tag message" m DEFINE_string 'messagefile' "" "use the contents of the given file as a tag message" f DEFINE_boolean 'push' false "push to $ORIGIN after performing finish" p - DEFINE_boolean 'pushproduction' false "push the production branch" - DEFINE_boolean 'pushdevelop' false "push the develop branch" - DEFINE_boolean 'pushtag' false "push the tag" + DEFINE_boolean 'pushproduction' false "push the production branch" 1 + DEFINE_boolean 'pushdevelop' false "push the develop branch" 2 + DEFINE_boolean 'pushtag' false "push the tag" 3 DEFINE_boolean 'keep' false "keep branch after performing finish" k - DEFINE_boolean 'keepremote' false "keep the remote branch" - DEFINE_boolean 'keeplocal' false "keep the local branch" + DEFINE_boolean 'keepremote' false "keep the remote branch" 4 + DEFINE_boolean 'keeplocal' false "keep the local branch" 5 DEFINE_boolean 'force_delete' false "force delete release branch after finish" D DEFINE_boolean 'notag' false "don't tag this release" n DEFINE_boolean 'nobackmerge' false "don't back-merge $MASTER_BRANCH, or tag if applicable, in $DEVELOP_BRANCH " b DEFINE_boolean 'squash' false "squash release during merge" S - DEFINE_boolean 'squash-info' false "add branch info during squash" - DEFINE_boolean 'ff-master' false "fast forward master branch if possible" + DEFINE_boolean 'squash-info' false "add branch info during squash" 6 + DEFINE_boolean 'ff-master' false "fast forward master branch if possible" 7 DEFINE_boolean 'edit' true "accept the auto-generated message on merging" e DEFINE_string 'tagname' "" "use the given tag name" T - DEFINE_boolean 'nodevelopmerge' false "don't merge $BRANCH into $DEVELOP_BRANCH " + DEFINE_boolean 'nodevelopmerge' false "don't merge $BRANCH into $DEVELOP_BRANCH " 8 # Override defaults with values from config gitflow_override_flag_boolean "release.finish.fetch" "fetch" @@ -817,27 +840,29 @@ git flow release finish [-h] [-F] [-s] [-u] [-m | -f] [-p] [-k] [-n] [-b] [-S] [ Finish a release branch -- h,help Show this help -showcommands! Show git commands while executing them +v,showcommands! Show git commands while executing them F,[no]fetch Fetch from origin before performing finish s,sign! Sign the release tag cryptographically u,signingkey! Use the given GPG-key for the digital signature (implies -s) m,message! Use the given tag message f,[no]messagefile= Use the contents of the given file as a tag message p,[no]push Push to origin after performing finish -[no]pushproduction Push the production branch -[no]pushdevelop Push the develop branch -[no]pushtag Push the tag +1,[no]pushproduction Push the production branch +2,[no]pushdevelop Push the develop branch +3,[no]pushtag Push the tag k,[no]keep Keep branch after performing finish -[no]keepremote Keep the remote branch -[no]keeplocal Keep the local branch +4,[no]keepremote Keep the remote branch +5,[no]keeplocal Keep the local branch D,[no]force_delete Force delete release branch after finish n,[no]tag Don't tag this release b,[no]nobackmerge Don't back-merge master, or tag if applicable, in develop S,[no]squash Squash release during merge -[no]ff-master Fast forward master branch if possible +6,[no]squash-info Add branch info during squash +7,[no]ff-master Fast forward master branch if possible e,[no]edit The --noedit option can be used to accept the auto-generated message on merging T,tagname! Use given tag name -nodevelopmerge! Don't back-merge develop branch +8,nodevelopmerge! Don't back-merge develop branch +9,[no]create-release Create a GitHub/GitLab release for the tag " # Define flags DEFINE_boolean 'fetch' false "fetch from $ORIGIN before performing finish" F @@ -846,21 +871,22 @@ nodevelopmerge! Don't back-merge develop branch DEFINE_string 'message' "" "use the given tag message" m DEFINE_string 'messagefile' "" "use the contents of the given file as a tag message" f DEFINE_boolean 'push' false "push to $ORIGIN after performing finish" p - DEFINE_boolean 'pushproduction' false "push the production branch" - DEFINE_boolean 'pushdevelop' false "push the develop branch" - DEFINE_boolean 'pushtag' false "push the tag" + DEFINE_boolean 'pushproduction' false "push the production branch" 1 + DEFINE_boolean 'pushdevelop' false "push the develop branch" 2 + DEFINE_boolean 'pushtag' false "push the tag" 3 DEFINE_boolean 'keep' false "keep branch after performing finish" k - DEFINE_boolean 'keepremote' false "keep the remote branch" - DEFINE_boolean 'keeplocal' false "keep the local branch" + DEFINE_boolean 'keepremote' false "keep the remote branch" 4 + DEFINE_boolean 'keeplocal' false "keep the local branch" 5 DEFINE_boolean 'force_delete' false "force delete release branch after finish" D DEFINE_boolean 'notag' false "don't tag this release" n DEFINE_boolean 'nobackmerge' false "don't back-merge $MASTER_BRANCH, or tag if applicable, in $DEVELOP_BRANCH " b DEFINE_boolean 'squash' false "squash release during merge" S - DEFINE_boolean 'squash-info' false "add branch info during squash" - DEFINE_boolean 'ff-master' false "fast forward master branch if possible" + DEFINE_boolean 'squash-info' false "add branch info during squash" 6 + DEFINE_boolean 'ff-master' false "fast forward master branch if possible" 7 DEFINE_boolean 'edit' true "accept the auto-generated message on merging" e DEFINE_string 'tagname' "" "use the given tag name" T - DEFINE_boolean 'nodevelopmerge' false "don't merge $BRANCH into $DEVELOP_BRANCH " + DEFINE_boolean 'nodevelopmerge' false "don't merge $BRANCH into $DEVELOP_BRANCH " 8 + DEFINE_boolean 'create-release' false "create a GitHub/GitLab release for the tag" 9 # Override defaults with values from config gitflow_override_flag_boolean "release.finish.fetch" "fetch" @@ -882,6 +908,7 @@ nodevelopmerge! Don't back-merge develop branch gitflow_override_flag_string "release.finish.message" "message" gitflow_override_flag_string "release.finish.messagefile" "messagefile" gitflow_override_flag_boolean "release.finish.nodevelopmerge" "nodevelopmerge" + gitflow_override_flag_boolean "release.finish.create-release" "create-release" # Parse arguments parse_args "$@" @@ -1305,3 +1332,83 @@ r,[no]remote Delete remote branch echo "- You are now on branch '$(git_current_branch)'" echo } + +# +# LBEM Edition: Sync command - synchronize release branch with remote +# +cmd_sync() { + OPTIONS_SPEC="\ +git flow release sync [-h] [-p] [] + +Synchronize release branch with its remote counterpart. +This will: + 1. Stash any uncommitted changes + 2. Pull updates from the remote release branch + 3. Push the release branch to origin (if -p flag or config) + 4. Restore stashed changes + +When is omitted the current branch is used, but only if it's a release branch. +-- +h,help! Show this help +showcommands! Show git commands while executing them +p,[no]push Push to origin after sync +" + local did_stash + + # Define flags + DEFINE_boolean 'push' false "push to $ORIGIN after sync" p + + # Override defaults with values from config + gitflow_override_flag_boolean "release.sync.push" "push" + + # Parse arguments + parse_args "$@" + + # Use current branch if no name is given + if [ "$VERSION" = "" ]; then + gitflow_use_current_branch_version + fi + + # Sanity checks + require_branch "$BRANCH" + + run_pre_hook "$VERSION" "$ORIGIN" "$BRANCH" + + # Step 1: Stash uncommitted changes + echo "Syncing release branch '$BRANCH'..." + gitflow_stash_save + did_stash=$? + + # Step 2: Fetch and pull from remote release branch + echo "Fetching from '$ORIGIN'..." + git_do fetch -q "$ORIGIN" || die "Could not fetch from '$ORIGIN'." + + if git_remote_branch_exists "$ORIGIN/$BRANCH"; then + echo "Pulling updates from '$ORIGIN/$BRANCH'..." + git_do pull --rebase -q "$ORIGIN" "$BRANCH" 2>/dev/null || { + warn "Could not pull from '$ORIGIN/$BRANCH'. You may need to resolve conflicts." + gitflow_stash_pop $did_stash + exit 1 + } + fi + + # Step 3: Push if requested + if flag push; then + echo "Pushing '$BRANCH' to '$ORIGIN'..." + git_do push "$ORIGIN" "$BRANCH" || warn "Could not push to '$ORIGIN'. You may need to push manually." + fi + + # Step 4: Restore stashed changes + gitflow_stash_pop $did_stash + + run_post_hook "$VERSION" "$ORIGIN" "$BRANCH" + + echo + echo "Summary of actions:" + echo "- Release branch '$BRANCH' was synced with '$ORIGIN/$BRANCH'" + if flag push; then + echo "- Release branch '$BRANCH' was pushed to '$ORIGIN'" + fi + echo "- You are now on branch '$(git_current_branch)'" + echo +} diff --git a/git-flow-version b/git-flow-version index e6647ee..38dbb63 100644 --- a/git-flow-version +++ b/git-flow-version @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -39,7 +40,7 @@ # -GITFLOW_VERSION=2.2.1 +GITFLOW_VERSION=2.2.1-lbem.3 initialize() { # A function can not be empty. Comments count as empty. @@ -59,7 +60,7 @@ For more specific help type the command followed by --help } cmd_default() { - echo "$GITFLOW_VERSION (CJS Edition)" + echo "$GITFLOW_VERSION (LBEM Edition, based on CJS/AVH)" } cmd_help() { diff --git a/gitflow-common b/gitflow-common index 8dd0b9f..257fcf7 100644 --- a/gitflow-common +++ b/gitflow-common @@ -11,6 +11,7 @@ # http://github.com/CJ-Systems/gitflow-cjs # # Authors: +# Copyright 2025 LBEM. All rights reserved. # Copyright 2003 CJ Systems. All rights reserved. # Copyright 2012-2019 Peter van der Does. All rights reserved. # @@ -319,6 +320,9 @@ gitflow_load_settings() { done; mv "$GITFLOW_CONFIG" "$GITFLOW_CONFIG".backup 2>/dev/null fi + + # LBEM Edition: Load settings from .gitflow file in repo root + gitflow_load_dotgitflow } # @@ -498,6 +502,652 @@ gitflow_override_flag_string() { return ${FLAGS_TRUE} } +# +# LBEM Edition: Additional functionality for sync, propose, and finish mode +# + +# gitflow_check_finish_mode() +# +# Validate finish mode config value +# +# Param $1: string Value to validate +# Return: 0=valid, 1=invalid +# +gitflow_check_finish_mode() { + local _value + _value="${1}" + case "${_value}" in + classic|propose|ask) + return 0 + ;; + *) + return 1 + ;; + esac +} + +# gitflow_get_finish_mode() +# +# Get the finish mode for a subcommand (feature, bugfix, etc.) +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string "classic"|"propose"|"ask" (defaults to "classic") +# +gitflow_get_finish_mode() { + local _mode _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _mode=$(git config --get gitflow.${_subcommand}.finish.mode 2>/dev/null) + [ -z "$_mode" ] && _mode=$(git config --get gitflow.finish.mode 2>/dev/null) + [ -z "$_mode" ] && _mode="classic" + + echo "$_mode" +} + +# gitflow_check_sync_strategy() +# +# Validate sync strategy config value +# +# Param $1: string Value to validate +# Return: 0=valid, 1=invalid +# +gitflow_check_sync_strategy() { + local _value + _value="${1}" + case "${_value}" in + rebase|merge) + return 0 + ;; + *) + return 1 + ;; + esac +} + +# gitflow_get_sync_strategy() +# +# Get the sync strategy for a subcommand (feature, bugfix, etc.) +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string "rebase"|"merge" (defaults to "rebase") +# +gitflow_get_sync_strategy() { + local _strategy _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _strategy=$(git config --get gitflow.${_subcommand}.sync.strategy 2>/dev/null) + [ -z "$_strategy" ] && _strategy=$(git config --get gitflow.sync.strategy 2>/dev/null) + [ -z "$_strategy" ] && _strategy="rebase" + + echo "$_strategy" +} + +# gitflow_get_propose_open() +# +# Check if browser should auto-open after propose +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string "true"|"false" (defaults to "true") +# +gitflow_get_propose_open() { + local _open _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _open=$(git config --bool --get gitflow.${_subcommand}.propose.open 2>/dev/null) + [ -z "$_open" ] && _open=$(git config --bool --get gitflow.propose.open 2>/dev/null) + [ -z "$_open" ] && _open="true" + + echo "$_open" +} + +# gitflow_get_propose_draft() +# +# Check if PRs should be created as drafts by default +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string "true"|"false" (defaults to "false") +# +gitflow_get_propose_draft() { + local _draft _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _draft=$(git config --bool --get gitflow.${_subcommand}.propose.draft 2>/dev/null) + [ -z "$_draft" ] && _draft=$(git config --bool --get gitflow.propose.draft 2>/dev/null) + [ -z "$_draft" ] && _draft="false" + + echo "$_draft" +} + +# gitflow_get_propose_assignee() +# +# Get default assignee for PRs +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string Assignee (empty string if not set) +# +gitflow_get_propose_assignee() { + local _assignee _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _assignee=$(git config --get gitflow.${_subcommand}.propose.assignee 2>/dev/null) + [ -z "$_assignee" ] && _assignee=$(git config --get gitflow.propose.assignee 2>/dev/null) + + echo "$_assignee" +} + +# gitflow_get_propose_reviewer() +# +# Get default reviewer for PRs +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string Reviewer (empty string if not set) +# +gitflow_get_propose_reviewer() { + local _reviewer _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _reviewer=$(git config --get gitflow.${_subcommand}.propose.reviewer 2>/dev/null) + [ -z "$_reviewer" ] && _reviewer=$(git config --get gitflow.propose.reviewer 2>/dev/null) + + echo "$_reviewer" +} + +# gitflow_get_propose_labels() +# +# Get default labels for PRs +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string Labels (comma-separated, empty string if not set) +# +gitflow_get_propose_labels() { + local _labels _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _labels=$(git config --get gitflow.${_subcommand}.propose.labels 2>/dev/null) + [ -z "$_labels" ] && _labels=$(git config --get gitflow.propose.labels 2>/dev/null) + + echo "$_labels" +} + +# gitflow_get_propose_autolabel() +# +# Check if branch type should be auto-added as label +# +# Param $1: string Subcommand name (e.g., "feature", "bugfix") +# Return: string "true"|"false" (defaults to "false") +# +gitflow_get_propose_autolabel() { + local _autolabel _subcommand + _subcommand="${1}" + + # Try subcommand-specific first, then global + _autolabel=$(git config --bool --get gitflow.${_subcommand}.propose.autolabel 2>/dev/null) + [ -z "$_autolabel" ] && _autolabel=$(git config --bool --get gitflow.propose.autolabel 2>/dev/null) + [ -z "$_autolabel" ] && _autolabel="false" + + echo "$_autolabel" +} + +# gitflow_detect_forge() +# +# Detect the forge type (github, gitlab, bitbucket) from remote URL +# +# Return: string "github"|"gitlab"|"bitbucket"|"unknown" +# +gitflow_detect_forge() { + local _remote_url + + _remote_url=$(git config --get remote.${ORIGIN}.url 2>/dev/null) + + case "$_remote_url" in + *github.com*|*github.*) + echo "github" + ;; + *gitlab.com*|*gitlab.*) + echo "gitlab" + ;; + *bitbucket.org*|*bitbucket.*) + echo "bitbucket" + ;; + *) + echo "unknown" + ;; + esac +} + +# gitflow_get_repo_url() +# +# Get the web URL of the repository +# +# Return: string The HTTPS URL of the repository +# +gitflow_get_repo_url() { + local _remote_url _web_url + + _remote_url=$(git config --get remote.${ORIGIN}.url 2>/dev/null) + + # Convert SSH URL to HTTPS URL + # git@github.com:user/repo.git -> https://github.com/user/repo + # https://github.com/user/repo.git -> https://github.com/user/repo + _web_url=$(echo "$_remote_url" | sed -e 's/^git@/https:\/\//' -e 's/\.git$//' -e 's/:/\//' -e 's/https\/\//https:\/\//') + + echo "$_web_url" +} + +# gitflow_open_url() +# +# Open a URL in the default browser (cross-platform) +# +# Param $1: string URL to open +# +gitflow_open_url() { + local _url + _url="$1" + + case "$(uname -s)" in + Darwin) + open "$_url" + ;; + Linux) + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$_url" + elif command -v gnome-open >/dev/null 2>&1; then + gnome-open "$_url" + else + warn "Could not detect the web browser to use." + fi + ;; + *MINGW*|*CYGWIN*|*MSYS*) + start "$_url" + ;; + *) + warn "Could not detect the web browser to use." + ;; + esac +} + +# gitflow_has_cli_tool() +# +# Check if a CLI tool (gh, glab) is available and authenticated +# +# Param $1: string Tool name ("gh", "glab") +# Return: 0=available, 1=not available +# +gitflow_has_cli_tool() { + local _tool + _tool="$1" + + case "$_tool" in + gh) + command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 + ;; + glab) + command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1 + ;; + *) + return 1 + ;; + esac +} + +# gitflow_stash_save() +# +# Stash any uncommitted changes (like git-town sync) +# +# Return: 0=stashed, 1=nothing to stash +# +gitflow_stash_save() { + local _status + + # Check if there are uncommitted changes + git_is_clean_working_tree + _status=$? + + if [ $_status -ne 0 ]; then + git_do stash push -m "gitflow: auto-stash before sync" || die "Could not stash changes" + return 0 + fi + return 1 +} + +# gitflow_stash_pop() +# +# Restore stashed changes if we stashed earlier +# +# Param $1: boolean Whether we stashed (0=yes, 1=no) +# +gitflow_stash_pop() { + local _did_stash + _did_stash="$1" + + if [ "$_did_stash" -eq 0 ]; then + git_do stash pop || warn "Could not restore stashed changes. Run 'git stash pop' manually." + fi +} + +# gitflow_prune_branches() +# +# Prune stale remote-tracking branches and optionally delete local branches +# that were tracking branches deleted on remote +# +# Param $1: string Prune behavior ("prompt"|"delete"|"keep") +# +gitflow_prune_branches() { + local _prune_behavior _stale_branches _branch _tracking _remote_branch _answer _skip_all + local _has_unmerged _base_branch + + _prune_behavior="${1:-prompt}" + _skip_all="false" + + # First, prune stale remote-tracking branches + git_do fetch --prune "$ORIGIN" 2>/dev/null + + # Find local branches that were tracking deleted remote branches + _stale_branches="" + for _branch in $(git_local_branches); do + # Get the remote tracking branch + _tracking=$(git config --get "branch.${_branch}.remote" 2>/dev/null) + _remote_branch=$(git config --get "branch.${_branch}.merge" 2>/dev/null) + + # Skip if no tracking info + [ -z "$_tracking" ] || [ -z "$_remote_branch" ] && continue + + # Convert refs/heads/X to X + _remote_branch="${_remote_branch#refs/heads/}" + + # Check if remote branch no longer exists + if [ "$_tracking" = "$ORIGIN" ] && ! git_remote_branch_exists "$ORIGIN/$_remote_branch"; then + _stale_branches="$_stale_branches $_branch" + fi + done + + # If no stale branches, we're done + [ -z "$_stale_branches" ] && return 0 + + # Handle stale branches based on behavior + for _branch in $_stale_branches; do + # Skip protected branches + if [ "$_branch" = "$MASTER_BRANCH" ] || [ "$_branch" = "$DEVELOP_BRANCH" ]; then + continue + fi + + # Skip if we're on this branch + if [ "$_branch" = "$(git_current_branch)" ]; then + warn "Cannot delete branch '$_branch' - currently checked out." + continue + fi + + # Check if branch has unmerged local commits + _base_branch=$(gitflow_config_get_base_branch "$_branch") + _base_branch=${_base_branch:-$DEVELOP_BRANCH} + _has_unmerged="false" + + # Branch has unmerged commits if it's not merged into base + if ! git_is_branch_merged_into "$_branch" "$_base_branch"; then + _has_unmerged="true" + fi + + case "$_prune_behavior" in + delete) + if [ "$_has_unmerged" = "true" ]; then + warn "Branch '$_branch' has unmerged local commits - keeping it safe." + else + echo "Deleting stale local branch '$_branch' (already merged)..." + git_do branch -d "$_branch" 2>/dev/null || warn "Could not delete '$_branch'." + fi + ;; + keep) + echo "Keeping stale local branch '$_branch' (remote was deleted)" + ;; + prompt|*) + if [ "$_skip_all" = "true" ]; then + echo "Keeping stale local branch '$_branch'" + continue + fi + if [ "$_has_unmerged" = "true" ]; then + printf "Branch '%s' was deleted on remote but has LOCAL UNMERGED COMMITS. [d]elete anyway, [k]eep, [s]kip all? " "$_branch" + else + printf "Branch '%s' was deleted on remote (already merged). [d]elete, [k]eep, [s]kip all? " "$_branch" + fi + read _answer + case "$_answer" in + d|D|delete) + if [ "$_has_unmerged" = "true" ]; then + git_do branch -D "$_branch" 2>/dev/null && echo "Force deleted local branch '$_branch'" + else + git_do branch -d "$_branch" 2>/dev/null && echo "Deleted local branch '$_branch'" + fi + ;; + s|S|skip) + _skip_all="true" + echo "Skipping all remaining stale branches" + ;; + *) + echo "Keeping stale local branch '$_branch'" + ;; + esac + ;; + esac + done +} + +# gitflow_load_dotgitflow() +# +# Load settings from .gitflow file in repo root if it exists +# This is called from gitflow_load_settings +# +gitflow_load_dotgitflow() { + local _dotgitflow_file _config_lines _config_line _key _value + + _dotgitflow_file="$GIT_CURRENT_REPO_DIR/.gitflow" + + if [ -f "$_dotgitflow_file" ]; then + _config_lines=$(git config --list --file="$_dotgitflow_file" 2>/dev/null) + for _config_line in ${_config_lines}; do + _key=${_config_line%%=*} + _value=${_config_line#*=} + # Only set if not already set in local config (local config takes precedence) + if ! git config --local --get "gitflow.${_key}" >/dev/null 2>&1; then + git_do config --local "gitflow.${_key}" "${_value}" 2>/dev/null + fi + done + fi +} + +# gitflow_create_pr() +# +# Create a pull request using CLI tool or fallback to URL +# +# Param $1: string Branch name +# Param $2: string Base branch +# Param $3: string Subcommand (feature, bugfix) for config +# Param $4: string Draft flag ("true"|"false", optional) +# Param $5: string Assignee (optional, supports @me) +# Param $6: string Reviewer (optional) +# Param $7: string Labels (optional, comma-separated) +# +gitflow_create_pr() { + local _branch _base _subcommand _forge _repo_url _pr_url _open_browser _title + local _draft _assignee _reviewer _labels _autolabel _gh_opts _glab_opts + + _branch="$1" + _base="$2" + _subcommand="$3" + + # Get options from parameters or config (parameters take precedence) + _draft="${4:-$(gitflow_get_propose_draft "$_subcommand")}" + _assignee="${5:-$(gitflow_get_propose_assignee "$_subcommand")}" + _reviewer="${6:-$(gitflow_get_propose_reviewer "$_subcommand")}" + _labels="${7:-$(gitflow_get_propose_labels "$_subcommand")}" + _autolabel=$(gitflow_get_propose_autolabel "$_subcommand") + + _forge=$(gitflow_detect_forge) + _repo_url=$(gitflow_get_repo_url) + _open_browser=$(gitflow_get_propose_open "$_subcommand") + _title="${_branch#*/}" # Remove prefix for title + + # Add branch type label if autolabel is enabled + if [ "$_autolabel" = "true" ]; then + if [ -n "$_labels" ]; then + _labels="${_labels},${_subcommand}" + else + _labels="$_subcommand" + fi + fi + + case "$_forge" in + github) + if gitflow_has_cli_tool "gh"; then + echo "Creating pull request using GitHub CLI..." + + # Build gh options + _gh_opts="--base \"$_base\" --head \"$_branch\" --title \"$_title\" --fill" + + if [ "$_draft" = "true" ]; then + _gh_opts="$_gh_opts --draft" + fi + + if [ -n "$_assignee" ]; then + _gh_opts="$_gh_opts --assignee \"$_assignee\"" + fi + + if [ -n "$_reviewer" ]; then + _gh_opts="$_gh_opts --reviewer \"$_reviewer\"" + fi + + if [ -n "$_labels" ]; then + _gh_opts="$_gh_opts --label \"$_labels\"" + fi + + eval gh pr create $_gh_opts + + if [ "$_open_browser" = "true" ]; then + gh pr view --web + fi + else + _pr_url="${_repo_url}/compare/${_base}...${_branch}?expand=1" + echo "GitHub CLI not available. Opening browser to create PR..." + echo "URL: $_pr_url" + if [ "$_open_browser" = "true" ]; then + gitflow_open_url "$_pr_url" + fi + fi + ;; + gitlab) + if gitflow_has_cli_tool "glab"; then + echo "Creating merge request using GitLab CLI..." + + # Build glab options + _glab_opts="--source-branch \"$_branch\" --target-branch \"$_base\" --title \"$_title\" --fill" + + if [ "$_draft" = "true" ]; then + _glab_opts="$_glab_opts --draft" + fi + + if [ -n "$_assignee" ]; then + _glab_opts="$_glab_opts --assignee \"$_assignee\"" + fi + + if [ -n "$_reviewer" ]; then + _glab_opts="$_glab_opts --reviewer \"$_reviewer\"" + fi + + if [ -n "$_labels" ]; then + _glab_opts="$_glab_opts --label \"$_labels\"" + fi + + eval glab mr create $_glab_opts + + if [ "$_open_browser" = "true" ]; then + glab mr view --web + fi + else + _pr_url="${_repo_url}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${_branch}&merge_request%5Btarget_branch%5D=${_base}" + echo "GitLab CLI not available. Opening browser to create MR..." + echo "URL: $_pr_url" + if [ "$_open_browser" = "true" ]; then + gitflow_open_url "$_pr_url" + fi + fi + ;; + bitbucket) + _pr_url="${_repo_url}/pull-requests/new?source=${_branch}&dest=${_base}" + echo "Opening browser to create PR on Bitbucket..." + echo "URL: $_pr_url" + if [ "$_open_browser" = "true" ]; then + gitflow_open_url "$_pr_url" + fi + ;; + *) + warn "Unknown forge. Cannot create PR automatically." + warn "Please create a PR manually for branch '$_branch' targeting '$_base'." + return 1 + ;; + esac + + return 0 +} + +# gitflow_create_release() +# +# Create a GitHub/GitLab release for a tag +# +# Param $1: string Tag name (e.g., "v1.0.0") +# Param $2: string Title (optional, defaults to tag name) +# Return: 0 on success, 1 on failure +# +gitflow_create_release() { + local _tag _title _forge _repo_url + + _tag="$1" + _title="${2:-$_tag}" + _forge=$(gitflow_detect_forge) + _repo_url=$(gitflow_get_repo_url) + + case "$_forge" in + github) + if gitflow_has_cli_tool "gh"; then + echo "Creating GitHub release for tag '$_tag'..." + gh release create "$_tag" --generate-notes --title "$_title" + return $? + else + echo "GitHub CLI not available. Creating release manually..." + echo "Visit: ${_repo_url}/releases/new?tag=${_tag}" + gitflow_open_url "${_repo_url}/releases/new?tag=${_tag}" + return 0 + fi + ;; + gitlab) + if gitflow_has_cli_tool "glab"; then + echo "Creating GitLab release for tag '$_tag'..." + glab release create "$_tag" --notes "Release $_tag" + return $? + else + echo "GitLab CLI not available. Creating release manually..." + echo "Visit: ${_repo_url}/-/releases/new?tag_name=${_tag}" + gitflow_open_url "${_repo_url}/-/releases/new?tag_name=${_tag}" + return 0 + fi + ;; + bitbucket) + echo "Bitbucket does not have native releases. Tag '$_tag' has been created." + return 0 + ;; + *) + warn "Unknown forge. Cannot create release automatically." + warn "Tag '$_tag' has been created. Please create a release manually if needed." + return 1 + ;; + esac +} + # gitflow_create_squash_message() # # Create the squash message, overriding the one generated by git itself