pax_global_header00006660000000000000000000000064152361515030014513gustar00rootroot0000000000000052 comment=a383ed7feec3c157b6272e2e3a95fb9ae620b16c sugarjar-4.0.0/000077500000000000000000000000001523615150300133325ustar00rootroot00000000000000sugarjar-4.0.0/.github/000077500000000000000000000000001523615150300146725ustar00rootroot00000000000000sugarjar-4.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001523615150300170555ustar00rootroot00000000000000sugarjar-4.0.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007001523615150300215440ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: bug assignees: jaymzh --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior including commands and output **Expected behavior** A clear and concise description of what you expected to happen. **Environment (please complete the following information):** - OS: - Output of `sj version` sugarjar-4.0.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000012741523615150300226060ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: "[RFE]" labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Contribution** Are you willing to write this feature? If so, would you need assistance? **Additional context** Add any other context or screenshots about the feature request here. sugarjar-4.0.0/.github/ISSUE_TEMPLATE/support-request.md000066400000000000000000000005411523615150300226010ustar00rootroot00000000000000--- name: Support request about: Use this to ask for help title: "[support]" labels: '' assignees: '' --- **Describe the problem** Please describe the problem you are having in as much detail as possible. **What you've tried** Please describe what steps you've taken to try to solve the problem **Version** Please provide the output of `sj version` sugarjar-4.0.0/.github/dependabot.yml000066400000000000000000000006171523615150300175260ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: bundler directory: / schedule: interval: monthly groups: all-minor-patch: patterns: ["*"] update-types: ["minor", "patch"] - package-ecosystem: github-actions directory: / schedule: interval: monthly groups: all-minor-patch: patterns: ["*"] update-types: ["minor", "patch"] sugarjar-4.0.0/.github/workflows/000077500000000000000000000000001523615150300167275ustar00rootroot00000000000000sugarjar-4.0.0/.github/workflows/check-all-checks.yml000066400000000000000000000007141523615150300225350ustar00rootroot00000000000000name: All checks pass on: pull_request: permissions: {} jobs: allchecks: runs-on: ubuntu-latest permissions: checks: read contents: read steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - uses: wechuli/allcheckspassed@204ff63e89eabdc8086f0c50b91f675bcf37c8f6 # v2.4.0 sugarjar-4.0.0/.github/workflows/codeql.yml000066400000000000000000000021131523615150300207160ustar00rootroot00000000000000name: "CodeQL" on: push: branches: ["main"] pull_request: branches: ["main"] schedule: - cron: "0 0 * * 1" permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["ruby"] steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: category: "/language:${{matrix.language}}" sugarjar-4.0.0/.github/workflows/dco.yml000066400000000000000000000013611523615150300202200ustar00rootroot00000000000000name: DCO Check on: [pull_request] permissions: {} jobs: dco_check_job: permissions: contents: read pull-requests: read runs-on: ubuntu-latest name: DCO Check steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Get PR Commits uses: actionshub/get-pr-commits@6fc8a0242c2c5853e3e8b86bde9d176682e5c5fb # main id: 'get-pr-commits' with: token: ${{ secrets.GITHUB_TOKEN }} - name: DCO Check uses: actionshub/dco@736d5885f972dcfd68b3db44da9a940b370cc625 # main with: commits: ${{ steps.get-pr-commits.outputs.commits }} sugarjar-4.0.0/.github/workflows/dependency-review.yml000066400000000000000000000011141523615150300230640ustar00rootroot00000000000000name: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Dependency Review' uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 sugarjar-4.0.0/.github/workflows/lint.yml000066400000000000000000000041431523615150300204220ustar00rootroot00000000000000name: Lint on: push: branches: [ main ] pull_request: branches: [ main ] permissions: {} jobs: # Rather than setup ruby several times, we run rubocop and mdl in # the same place, until the actionshub mdl checker is working again ruby-linters: permissions: contents: read pull-requests: read strategy: fail-fast: false runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup ruby uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 with: ruby-version: '3.2' - name: install deps run: bundle install - name: Run rubocop run: ./scripts/run_rubocop.sh - name: Run mdl run: ./scripts/run_mdl.sh #markdownlint: # permissions: # contents: read # pull-requests: read # runs-on: ubuntu-latest # steps: # - name: Harden the runner (Audit all outbound calls) # uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 # with: # egress-policy: audit # - name: checkout # uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - name: MarkdownLint mdl Action # uses: actionshub/markdownlint@6c82ff529253530dfbf75c37570876c52692835f # 3.1.4 linelint: permissions: contents: read pull-requests: read runs-on: ubuntu-latest name: Check if all files end in newline steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Linelint uses: fernandrone/linelint@7907a5dca0c28ea7dd05c6d8d8cacded713aca11 # master id: linelint sugarjar-4.0.0/.github/workflows/unit.yml000066400000000000000000000017461523615150300204410ustar00rootroot00000000000000name: Unittests on: push: branches: [ main ] pull_request: branches: [ main ] permissions: {} jobs: rspec: permissions: contents: read pull-requests: read strategy: fail-fast: false matrix: ruby: [3.2, 3.3, 3.4] runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Install system dependencies run: |- sudo apt update sudo apt install -y gh glab - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Ruby uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 with: ruby-version: ${{ matrix.ruby }} - name: Install Ruby dependencies run: bundle install - name: Run rspec run: ./scripts/run_rspec.sh sugarjar-4.0.0/.gitignore000066400000000000000000000023361523615150300153260ustar00rootroot00000000000000*.gem *.rbc /.config /coverage/ /InstalledFiles /pkg/ /spec/reports/ /spec/examples.txt /test/tmp/ /test/version_tmp/ /tmp/ # Used by dotenv library to load environment variables. # .env # Ignore Byebug command history file. .byebug_history ## Specific to RubyMotion: .dat* .repl_history build/ *.bridgesupport build-iPhoneOS/ build-iPhoneSimulator/ ## Specific to RubyMotion (use of CocoaPods): # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # vendor/Pods/ ## Documentation cache and generated files: /.yardoc/ /_yardoc/ /doc/ /rdoc/ ## Environment normalization: /.bundle/ /vendor/bundle /lib/bundler/man/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # Gemfile.lock # .ruby-version # .ruby-gemset # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc # Used by RuboCop. Remote config files pulled in from inherit_from directive. # .rubocop-https?--* packaging/.vagrant noarch .ruby-version sugarjar-4.0.0/.linelint.yml000066400000000000000000000001011523615150300157410ustar00rootroot00000000000000rules: end-of-file: enable: true single-new-line: true sugarjar-4.0.0/.mdl_style.rb000066400000000000000000000001771523615150300157360ustar00rootroot00000000000000all rule 'MD013', :ignore_code_blocks => true rule 'MD026', :punctuation => '.,:;' exclude_rule 'MD041' exclude_rule 'MD033' sugarjar-4.0.0/.mdlrc000066400000000000000000000000541523615150300144330ustar00rootroot00000000000000style '.mdl_style.rb' parse_block_html true sugarjar-4.0.0/.rubocop.yml000066400000000000000000000022221523615150300156020ustar00rootroot00000000000000AllCops: TargetRubyVersion: 3.2 NewCops: enable Exclude: - 'rubygem-sugarjar.spec' Layout/LineLength: Max: 80 Metrics/BlockNesting: Enabled: false Metrics/ParameterLists: Enabled: false Naming/FileName: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/AbcSize: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/MethodLength: Enabled: false Metrics/ClassLength: Enabled: false Metrics/BlockLength: Enabled: false Style/FrozenStringLiteralComment: EnforcedStyle: never Style/LineEndConcatenation: Enabled: false Style/StringConcatenation: Enabled: false Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: comma Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma Style/HashSyntax: EnforcedStyle: hash_rockets Style/PercentLiteralDelimiters: PreferredDelimiters: default: '{}' '%i': '{}' '%I': '{}' '%w': '{}' '%W': '{}' '%r': '{}' Style/TrailingCommaInArguments: EnforcedStyleForMultiline: comma Style/Documentation: Enabled: false Metrics/PerceivedComplexity: Enabled: false Layout/DotPosition: EnforcedStyle: trailing sugarjar-4.0.0/.sugarjar.yaml000066400000000000000000000001221523615150300161050ustar00rootroot00000000000000on_push: [lint] lint_list_cmd: scripts/get_linters unit: - scripts/run_rspec.sh sugarjar-4.0.0/CHANGELOG.md000066400000000000000000000164111523615150300151460ustar00rootroot00000000000000# SugarJar Changelog ## 4.0.0 (2026-08-09) * Rework config file format to be more friendly to working with multiple forges * Add `modernizeconfig` command to help users migrate to new config file format * Various improvements to gitlab compatibility * Fix bugs around repo name detection when directory name doesn't match * Revamp `help` to be easier to read and navigate * Add support for Forgejo/Codeberg forges * Update repoconfig format to allow specifying names for checks ## 3.0.1 (2026-06-19) * Update tests for more resiliency in various package-building environments * Better fallbacks for forge type * Add config option to disable fork-based flows * Fix false-positive for "primary branch doesn't match" error * gitlab: Handle 3-level repos better * gitlab: Fix `smartpullrequest` issues * gitlab: significant improvement to `smartclone` * Add `--fork-name` option to enable forking to a different repo name ## 3.0.0 (2026-06-08) * Add support for GitLab * Add release-branch handling * Fixes to `sync`/`fsync` to not leave repo in consistent state * When in manual lint/unit, allow user to skip amending, and keep testing * Fix typos in various messages ## 2.0.2 (2026-01-08) * Fix `branchclean` logic to properly compare with the target branch (might have refused to clean branches that could be cleaned) * Add new commands to handle remote branch cleanup as well as rename `bclean` (keeping backwards compatible aliases): * `localbranchclean` / `lbclean` - local branch clean. aliased as `bclean` for back-comat * `localbranchcleanall` / `lbcleanall` - local all branch clean aliased as `bcleanall` for back-compat * `remotebranchclean` / `rbclean` - remote branch clean * `remotebranchcleanall` / `rbcleanall` - remote all branch clean * `globalbranchclean` / `gbclean` - local+remote branch clean * `globalbranchcleanall` / `gbcleanall` - local+remote all branch clean * Added new `sync` command to aid syncing branches across multiple workstations, see help for details. * Fix meta-ref handling which fixes crashes when using `smartlog` during rebases * Handle worktress gracefully when doing branch cleans * Make unittests work properly outside of git repos ## 2.0.1 (2025-05-12) * Fix gemspec to include new library files ## 2.0.0 (2025-05-11) * Fix smartlog when on detached head * Drop support for `hub`, and thus also `fallthru` mode * Fix GHE handling when using `gh` * Support `github_host` and `github_user` in repoconfig * Replace `version` subcommand with `debuginfo` subcommand (`--version` still exists) * `smartclone`: set upstream for main branch to upstream remote when applicable * Warn when deprecated options found in config file * Fix handling of `--color` in some cornercases * `subfeature` PRs: Fix bug where we would incorrectly deterine base branch * Checks: Fix bug where we would lint even if repo was dirty causing confusing output * `feature` prefixes: Fix bug where we didn't look for the prefix on the base branch when specified * Better handle creating PRs to branches other than "main" * Significantly improve unittest coverage * Bump required Ruby to 3.2 ## 1.1.3 (2025-02-20) * smartpullrequest: When working with `gh`, bypass its attempt to push, bypassing unnecessary prompts and branch track mangling * smartpullrequest: Better support for autofill * smartpullrequest: Don't attempt to stack when in forked repo ## 1.1.2 (2024-04-25) * Add support for 'subfeatures' * Add support for building stacked PRs based on 'subfeatures' * smartpullrequest: only autofill in the PR when a single commit exists between the base and us * smartpullrequest: Add `--fill` option to let people opt-out of autofilling the PR * smartpullrequest: State that we're autofilling the PR when we do * feature: Fix some corner cases where feature-prefixing didn't work * pullsuggestions: Print the diff in the correct order * feature/subfeature: set tracked branch for the user * subfeature: automatically update tracked branch when previous tracked branch disappears ## 1.1.1 (2024-02-12) * Relax ruby requirements to allow for easier packaging * Handle aborted rebases better * Add bash-completion script * Various doc updates ## 1.1.0 (2023-12-31) * Fix include path for unittests for downstream packagers * Bump ruby min versions * Include Gemfile.lock for downstream packagers ## 1.0.1 (2023-12-20) * `co` support for featureprefix * Add `include_from` and `overwrite_from` support to repoconfig * Support relative paths for lints/units * `smartpr` now uses `--fill` ## 1.0.0 (2023-10-22) * Add new "feature prefix" feature * Implement `auto` setting for `github_cli`, default to `gh` * Point people to Sapling * Handle `sclone` of repos in personal orgs * Better error when a subcommand isn't specified * Various documentation fixes ## 0.0.11 (2022-10-06) * Properly handle slashes in branch names (closes #101) * Support for running a command to determine checks (linters, units) to run * Support for using `gh` CLI instead of `hub` (experimental) * Add new `pullsuggestions` command to pull in (accepted) suggestions from a GitHub code review. * Detect mismatched primary branch names to assist with projects changing from `master` to `main` ## 0.0.10 (2021-12-06) * Support 'main' as a default/primary branch * Fix doc errors * Handle rebase failures more gracefully, give users hints (closes #88) * Handle SAML errors better (closes #95) * Don't parse option args as subcommands (closes #89) ## 0.0.9 (2021-02-20) * Fix smartclone not honoring `--github-host` * Use SSH protocol by default on short repo names * Handle anonymous auth failures gracefully * Better support for autocorrecting linters ## 0.0.8 (2020-12-16) * Colorize and simplify output * New smartlog feature * Doc fixes ## 0.0.7 (2020-11-23) * Add new command `smartpullrequest` (or `smartpr` or `spr`) for creating pull requests (closes #51) * Add checks for dirty repos before `smartpush`, `forcepush`, and `smartpullrequest` * Add `--ignore-dirty` and `--ignore-prerun-failure` options * Handle when git prompts for a username (closes #52) * Always use SSH for the forked remote (closes #56) * Better handling of various forms of repo URLs * Fix typo of `version` in help message * Fix typos in `README.md` ## 0.0.6 (2020-07-05) * Add automatic commit template configuration (closes #38) * bcleanall: Return to reasonable branch (fixes #37) * Handle case where `hub` has no auth token (fixes #39) * Fix crash in `smartclone` * Improve logging * Fix `sj unit` running lints instead of units ## 0.0.5 (2020-06-24) * Fix global config file handling * Better logging around lint/unit failuers * Handle incorrect tracked branches better ## 0.0.4 (2020-06-17) * Fix gemspec to include executables * Add support for building omnibus releases ## 0.0.3 (2020-06-08) * Stop rescuing NoMethodError (fixing a variety of confusing error cases) * Fix crash when no `on_push` entry is in repo config * Document contribution process (`CONTRIBUTING.md`) * Document code of conduct (`CODE_OF_CONDUCT.md`) ## 0.0.2 (2020-06-06) * Fix 'co' not accepting multiple arguments/options * Fix README typos (#10, #11) * Don't assume the ruby to run under * Don't crash when no subcommands are passed in * Don't assume paths (e.g. for hub, git) * Fix crash for unknown method * fix handling of empty config files ## 0.0.1 (2020-06-05) * Initial release sugarjar-4.0.0/CODE_OF_CONDUCT.md000066400000000000000000000064241523615150300161370ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at phil@ipom.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) For answers to common questions about this code of conduct, see [Contributor Covenant](https://www.contributor-covenant.org) sugarjar-4.0.0/CONTRIBUTING.md000066400000000000000000000015661523615150300155730ustar00rootroot00000000000000# Contributing to SugarJar We welcome contributions! Contributions come in a variety of forms: clear bug reports, code, or spreading the word about this project. If you'd like to contribute code, here's how. Simply use SugarJar to make a fork and setup your repo: ```shell sj sclone jaymzh/sugarjar ``` Make a branch for your change: ```shell sj feature mychange ``` Make whatever changes you want, commit with a clear commit message, and a DCO. We require [Developer Certificate of Origin (DCO)](https://developercertificate.org/) via a 'signed-off-by:` line in your commit (the `git commit -s` does this for you). The Chef community has a lot of great documentation on this which you can find [here](https://docs.chef.io/community_contributions/#developer-certification-of-origin-dco). ```shell git commit -as ``` Make a pull request: ```shell sj spush sj pull-request ``` sugarjar-4.0.0/Gemfile000066400000000000000000000001721523615150300146250ustar00rootroot00000000000000source 'https://rubygems.org' gem 'sugarjar', :path => '.' group :test do gem 'mdl' gem 'rspec' gem 'rubocop' end sugarjar-4.0.0/Gemfile.lock000066400000000000000000000043241523615150300155570ustar00rootroot00000000000000PATH remote: . specs: sugarjar (4.0.0) deep_merge diffy mixlib-log mixlib-shellout pastel GEM remote: https://rubygems.org/ specs: ast (2.4.3) chef-utils (19.3.15) concurrent-ruby concurrent-ruby (1.3.8) deep_merge (1.2.2) diff-lcs (1.6.2) diffy (3.4.4) ffi (1.17.4) ffi (1.17.4-arm64-darwin) ffi (1.17.4-x86_64-darwin) ffi (1.17.4-x86_64-linux-gnu) json (2.21.2) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) language_server-protocol (3.17.0.6) lint_roller (1.1.0) mdl (0.18.0) kramdown (~> 2.5) kramdown-parser-gfm (~> 1.1) mixlib-cli mixlib-config mixlib-shellout uri mixlib-cli (2.1.8) mixlib-config (3.0.27) tomlrb mixlib-log (3.2.3) ffi (>= 1.15.5) mixlib-shellout (3.4.10) chef-utils parallel (2.1.0) parser (3.3.12.0) ast (~> 2.4.1) racc pastel (0.8.0) tty-color (~> 0.5) prism (1.9.0) racc (1.8.1) rainbow (3.1.1) regexp_parser (2.12.0) rexml (3.4.4) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.7) rubocop (1.89.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.50.0) parser (>= 3.3.7.2) prism (~> 1.7) ruby-progressbar (1.13.0) tomlrb (2.0.4) tty-color (0.6.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) uri (1.1.1) PLATFORMS arm64-darwin ruby x86_64-darwin x86_64-linux DEPENDENCIES mdl rspec rubocop sugarjar! BUNDLED WITH 2.6.4 sugarjar-4.0.0/LICENSE000066400000000000000000000261271523615150300143470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2020-present Phil Dibowitz Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. sugarjar-4.0.0/README.md000066400000000000000000000533141523615150300146170ustar00rootroot00000000000000# SugarJar [![Lint]( https://github.com/jaymzh/sugarjar/workflows/Lint/badge.svg )](https://github.com/jaymzh/sugarjar/actions?query=workflow%3ALint) [![Unittest]( https://github.com/jaymzh/sugarjar/workflows/Unittests/badge.svg )](https://github.com/jaymzh/sugarjar/actions?query=workflow%3AUnittests) [![DCO]( https://github.com/jaymzh/sugarjar/workflows/DCO%20Check/badge.svg )](https://github.com/jaymzh/sugarjar/actions?query=workflow%3A%22DCO+Check%22) [![CodeQL]( https://github.com/jaymzh/sugarjar/actions/workflows/codeql.yml/badge.svg )](https://github.com/jaymzh/sugarjar/actions/workflows/codeql.yml) Welcome to SugarJar - a git + github/gitlab helper. The only requirements are Ruby, `git`, and either [gh](https://cli.github.com/) or [glab](https://docs.gitlab.com/cli/), depending on which forge you are using. SugarJar is inspired by [arcanist](https://github.com/phacility/arcanist), and its replacement at Facebook, JellyFish. Many of the features they provide for the Phabricator workflow this aims to bring to the GitHub workflow. In particular there are a lot of helpers for using a squash-merge workflow that is poorly handled by the standard toolsets. If you miss Mondrian or Phabricator - this is the tool for you! If you don't, there's a ton of useful stuff for everyone! Jump to what you're most interested in: > [!NOTE] > There is a new 'modernizeconfig' option to automatically convert your > config for the significant changes in 4.x. * [Common Use-cases](#common-use-cases) * [Auto Cleanup Squash-merged branches](#auto-cleanup-squash-merged-branches) * [Smarter clones and remotes](#smarter-clones-and-remotes) * [Work with stacked branches more easily]( #work-with-stacked-branches-more-easily) * [Creating Stacked PRs with subfeatures]( #creating-stacked-prs-with-subfeatures) * [Smart release branch handling]( #smart-release-branch-handling) * [Have a better lint/unittest experience!]( #have-a-better-lintunittest-experience) * [Better push defaults](#better-push-defaults) * [Cleaning up your own history](#cleaning-up-your-own-history) * [Better feature branches](#better-feature-branches) * [Smartlog](#smartlog) * [Sync work across workstations](#sync-work-across-workstations) * [Pulling in suggestions from the web](#pulling-in-suggestions-from-the-web) * [And more!](#and-more) * [Installation](#installation) * [Configuration](#configuration) * [Repository Configuration](#repository-configuration) * [Commit Templates](#commit-templates) * [Enterprise GitHub](#enterprise-github) * [FAQ](#faq) ## Common Use-cases ### Auto cleanup squash-merged branches
Clean-up squash-merged PRs safely and easily It is common for a PR to go back and forth with a variety of nits, lint fixes, typos, etc. that can muddy history. So many projects will "squash and merge" when they accept a pull request. However, that means `git branch -d BRANCH` doesn't work. Git will tell you the branch isn't fully merged. You can, of course `git branch -D BRANCH`, but that does no safety checks at all, it forces the deletion. Enter `sj lbclean` - it determines if the contents of your branch has been merge and safely deletes if so. (Note: `lbclean` stands for "local branch clean", and is aliased to `bclean` for both backwards-compatibility and also since it's the most common branch-cleanup command). ![bclean screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/bclean.png ) Will delete a branch, if it has been merged, **even if it was squash-merged**. You can pass it a branch if you'd like (it defaults to the branch you're on): `sj bclean BRANCH`. But it gets better! You can use `sj bcleanall` to remove all branches that have been merged: ![bcleanall screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/bcleanall.png ) *NOTE*: You can add long-lived release-branches to your RepoConfig to prevent cleaning; see [Smart release branch handling](️#smart-release-branch-handling). There is also `sj rbclean` ("remote branch clean") (and `sj rbcleanall`) for cleanup of remote branches. *Note*: This cannot differentiate between PR/feature branches which have been merged and long-lived release branches that have been merged (e.g. if '2.0-release' is a branch and has no commits not in main, it will be deleted). There is even `sj gbclean` ("global branch clean") (and `sj gbcleanall`) which will do both the local and remote cleaning. *NOTE*: Remote branch cleaning is still experimental, use with caution!
### Smarter clones and remotes
Fork, clone, and add the upstream remote automatically There's a pattern to every new repo we want to contribute to. First we fork, then we clone the fork, then we add a remote of the upstream repo. It's monotonous. SugarJar does this for you: ![smartclone screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/sclone.png ) `sj` accepts both `smartclone` and `sclone` for this command. This will: * Fork the repo to your personal org (if you don't already have a fork, and if it's not already in your personal org) * Clone your fork * Add the original as an 'upstream' remote Note that if you pass in a full-formed URL (like `git@github.com:chef/omnibus-toolchain.git`), SugarJar will infer the host and the forge type (gitub, gitlab, etc.). However, it does accept short names for repos ($org/$repo) - however, for cloning, since there is no repo yet for SugarJar to determine the forge from, you will need either to specify `--default-forge-host`, or have `default_forge_host` set in your config file. Like `git clone`, `sj smartclone` will accept an additional argument as the destination directory to clone to. It will also pass any other unknown options to `git clone` under the hood. If you don't work with fork-based workflows, you can set `use_forks: false` in your config for the right host: ```yaml host_configs: default: use_forks: false ``` See [Configuration](#Configuration) for more details on the config file.
### Work with stacked branches more easily
Create and manage stacked/dependent branches easily It's important to break changes into reviewable chunks, but working with stacked branches can be confusing. SugarJar provides several tools to make this easier. First, and foremost, is `feature` and `subfeature`. Regardless of stacking, the way to create a new feature bracnh with sugarjar is with `sj feature` (or `sj f` for short): ![feature screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/feature.png ) A "feature" in SugarJar parlance just means that the branch is always created from "most main" - this is usually `upstream/main`, but SJ will figure out which remote is the "upstream", even if it's `origin`, and then will determine the primary branch (`main` or for older repos `master`). It's also smart enough to fetch that remote first to make sure you're working on the latest HEAD. When you want to create a stacked PR, you can create `subfeature`, which, at its core is just a branch created from the current branch: ![subfeature screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature.png ) If you create branches like this then sugarjar can now make several things much easier: * `sj up` will rebase intelligently * After an `sj bclean` of a branch earlier in the tree, `sj up` will update the tracked branch to "most main" There are two commands that will show you the state of your stacked branches: * `sj binfo` - shows the current branch and its ancestors up to your primary branch * `sj smartlog` (aka `sj sl`) - shows you the whole tree. To continue with the example above, my `smartlog` might look like: ![subfeature-smartlog screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature-smartlog.png ) As you can see, `mynewthing` is derived from `main`, and `dependentnewthing` is derived from `mynewthing`. Now lets make a different feature stack: ![subfeature-part2 screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature-part2.png ) The `smartlog` will now show us this tree, and it's a bit more interesting: ![subfeature-part2-smartlog screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature-part2-smartlog.png ) Here we can see from `main`, we have two branches: one going to `mynewthing` and one going to `anotherfeature`. Each of those has their own dependent branch on top. Now, what happens if I make a change to `mynewthing` (the bottom of the first stack)? ![subfeature-part3 screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature-part3.png ) We can see here now that `dependentnewthing`, is based off a commit that _used_ to be `mynewthing` (`5086ee`), but `mynewthing` has moved. Both `mynewthing` and `dependentnewthing` are derived from `5086ee` (the old `mynewthing`), but `dependentnewthing` isn't (yet) based on the current `mynewthing`. But SugarJar will handle this all correctly when we ask it to update the branch: ![subfeature-part3-rebase screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature-part3-rebase.png ) Here we see that SugarJar knew that `dependentnewthing` should be rebased onto `mynewthing`, and it did the right thing - from main there's still the `50806ee` _and_ the new additional change which are now both part of the `mynewthing` branch, and `dependentnewthing` is based on that branch, this including all 3 commits in the right order. Now, lets say that `mynewthing` gets merged and we use `bclean` to clean it all up, what happens then? ![subfeature-detect-missing-base screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/subfeature-detect-missing-base.png ) SugarJar detects that branch is gone and thus this branch should now be based on the upstream main branch!
### Creating Stacked PRs with subfeatures
Automatically base PRs for subfeatures on their parent branch When dependent branches are created with `subfeature`, when you create a PR, SugarJar will automatically set the 'base' of the PR to the parent branch. By default it'll prompt you about this, but you can set `pr_autostack` to `true` in your config to tell it to always do this (or `false` to never do this): ```text $ sj spr Autofilling in PR from commit message It looks like this is a subfeature, would you like to base this PR on mynewthing? [y/n] y ... ```
### Smart release branch handling
Configure and protect long-lived release branches You can tell sugar what release branches exist, and it will intelligently handle them. So of you specify, in your repoconfig: ```yaml release_branches: ['v1-branch', 'v2-branch'] ``` Then: * `sj feature v1-backport-foo v2-branch` will automatically base this branch on `upstream/v2-branch` (or `origin/v2-branch` as appropriate) * `sj feature v2-branch` will checkout `v2-branch` with the upstream set to `upstream/v2-branch` (or `origin/v2-branch` as appropriate) * `sj lbclean`/`sj lbcleanall` (of all varieties) will never reap release branches
### Have a better lint/unittest experience!
Run lint and unittests automatically before pushing Ever made a PR, only to find out later that it failed tests because of some small lint issue? Not anymore! SJ can be configured to run things before pushing. For example,in the SugarJar repo, we have it run Rubocop (ruby lint) and Markdownlint `on_push`. If those fail, it lets you know and doesn't push. You can configure SugarJar to tell it how to run both lints and unittests for a given repo and if one or both should be run prior to pushing. The details on the config file format is below, but we provide three commands: ```shell sj lint ``` Run all linters. ```shell sj unit ``` Run all unittests. ```shell sj smartpush # or spush ``` Run configured push-time actions (nothing, lint, unit, both), and do not push if any of them fail.
### Better push defaults
Smart defaults for `smartpush` In addition to running pre-push tests for you `smartpush` also picks smart defaults for push. So if you `sj spush` with no arguments, it uses the `origin` remote and the same branch name you're on as the remote branch.
### Cleaning up your own history
Safely amend commits and force-push with `amend`/`fpush` Perhaps you contribute to a project that prefers to use merge commits, so you like to clean up your own history. This is often difficult to get right - a combination of rebases, amends and force pushes. We provide two commands here to help. The first is pretty straight forward and is basically just an alias: `sj amend`. It will amend whatever you want to the most recent commit (just an alias for `git commit --amend`). It has a partner `qamend` (or `amendq` if you prefer) that will do so without prompting to update your commit message. So now you've rebased or amended, pushing becomes challenging. You can `git push --force`, but everyone knows that's incredibly dangerous. Is there a better way? There is! Git provides `git push --force-with-lease` - it checks to make sure you're up-to-date with the remote before forcing the push. But man that command is a mouthful! Enter `sj fpush`. It has all the smarts of `sj smartpush` (runs configured pre-push actions), but adds `--force-with-lease` to the command!
### Better feature branches
Create feature branches with the right base and prefix When you want to start a new feature, you want to start developing against latest. That's why `sj feature` defaults to creating a branch against what we call "most master". That is, `upstream/master` if it exists, otherwise `origin/master` if that exists, otherwise `master`. You can pass in an additional argument to base it off of something else. ```shell $ git branch master test1 test2 * test2.1 test3 $ sj feature test-branch Created feature branch test-branch based on origin/master $ sj feature dependent-feature test-branch Created feature branch dependent-feature based on test-branch ``` Additionally you can specify a `feature_prefix` in your config which will cause `feature` to create branches prefixed with your `feature_prefix` and will also cause `co` to checkout branches with that prefix. This is useful when organizations use branch-based workflows and branches need to be prefixed with e.g. `$USER/`. For example, if your prefix was `user/`, then `sj feature foo` would create `user/foo`, and `sj co foo` would switch to `user/foo`. Here's an example: ```yaml host_configs: default: feature_prefix: "jsmith/" ```
### Smartlog
Visualize your branch tree Smartlog will show you a tree diagram of your branches! Simply run `sj smartlog` or `sj sl` for short. ![smartlog screenshot]( https://github.com/jaymzh/sugarjar/blob/main/images/smartlog.png )
### Sync work across workstations
Keep branches in sync across multiple machines If you work on multiple workstations, keeping your branches in-sync can be a pain. SugarJar provides `sync` to help with this. For example, if you do some work on feature `foo` on machine1 and push to `origin/foo` (intending to eventually merge to `upstream/main`), then on machine2, you pull that branch, do more work, which you also push to `origin/foo`, then on machine1, you can do `sj sync` to pull down the changes from `origin/foo`. If you have local changes, that are not already on `origin/foo`, those will be rebased on top of the changes from `origin/foo`. It's very similar to `sj up`, but instead of rebasing on top of the tracking branch, it rebases on top of the push target branch.
### Pulling in suggestions from the web
Merge in GitHub/GitLab suggested changes When someone 'suggests' a change in the GH/GL WebUI, once you choose to commit them, your origin and local branches are no longer in-sync. The `pullsuggestions` command will attempt to merge in any remote commits to your local branch. This command will show a diff and ask for confirmation before attempting the merge and - if allowed to continue - will use a fast-forward merge.
### Modernize your config
Convert your old config to the new `host_configs` format In 4.0, we significantly revamped how the configuration works, adding the new `host_configs` section, and deprecating or moving many top-level configs. In order to make this transition easier, there is now a `modernizeconfig` command to convert it for you! ```shell sj modernizeconfig ~/.config/sugarjar/config.yml ``` This will generate a new config for you, show you a diff, and offer to either update the existing file, or save it off the side for you.
### And more!
There's even more than this! See `sj help` for more commands!
## Installation Sugarjar is packaged in a variety of Linux distributions - see if it's on the list here, and if so, use your package manager (or `gem`) to install it: [![Packaging status]( https://repology.org/badge/vertical-allrepos/sugarjar.svg?exclude_unsupported=1 )](https://repology.org/project/sugarjar/versions) If you are using a Linux distribution version that is end-of-life'd, click the above image, it'll take you to a page that lists unsupported distro versions as well (they'll have older SugarJar, but they'll probably still have some version). **Ubuntu users**: You can use [this PPA]( https://launchpad.net/~michel-slm/+archive/ubuntu/sugarjar) to get newer versions for all supported Ubuntu releases (as well as some older versions). Ubuntu package maintainer. **MacOS users**: We recommend using Homebrew - we keep SugarJar updated in Homebrew Core. Finally, if none of those work for you, you can clone this repo and run it directly from there. ## Configuration Sugarjar will read in both a system-level config file (`/etc/sugarjar/config.yaml`) and a user-level config file (`~/.config/sugarjar/config.yaml`), if they exist. Anything in the user config will override the system config, and command-line options override both. The yaml file is a straight key-value pair of options without their '--'. See [examples/sample_config.yaml](examples/sample_config.yaml) for an example configuration file. In addition, the environment variable `SUGARJAR_LOGLEVEL` can be defined to set a log level. This is primarily used as a way to turn debug on earlier in order to troubleshoot configuration parsing. Deprecated fields will cause a warning, but you can suppress that warning by defining `ignore_deprecated_options`, for example: ```yaml old_option: foo ignore_deprecated_options: - old_options ``` ## Repository Configuration Sugarjar looks for a `.sugarjar.yaml` in the root of the repository to tell it how to handle repo-specific things. See [examples/sample_repoconfig.yaml](examples/sample_repoconfig.yaml) for an example configuration that walks through all valid repo configurations in detail. ### Commit Templates While GitHub provides a way to specify a pull-request template by putting the right file into a repo, there is no way to tell git to automatically pick up a commit template by dropping a file in the repo. Users must do something like: `git config commit.template `. Making each developer do this is error prone, so this setting will automatically set this up for each developer. ## Enterprise GitHub/GitLab Like `gh` and `glab`, SugarJar supports Enterprise versions of GitHub and GitLab. In fact, we provide extra features just for it. By default, SugarJar will pick up the appropriate information from the repo itself, so the only real interesting part is `smartclone`. In most cases, SugarJar will automatically determine the proper forge-host based on the URL you pass in. However, if you use short names ("repo/org" or "company/repo/org"), then you can either pass in `--default-forge-host`, or, if you want to always default to a given host, you can set a top-level `default_forge_host` in your SugarJar config: So, for example you might have: ```yaml default_forge_host: gh.sample.com ``` ## FAQ **Why the name SugarJar?** It's mostly a backronym. Like jellyfish, I wanted two letters that were on home row on different sides of the keyboard to make it easy to type. I looked at the possible options that where there and not taken and tried to find one I could make an appropriate name out of. Since this utility adds lots of sugar to git and github/gitlab, it seemed appropriate. **I'd like to package SugarJar for my favorite distro/OS, is that OK?** Of course! But I'd appreciate you emailing me to give me a heads up. Doing so will allow me to make sure it shows up in the Repology badge above. **What platforms does it work on?** Since it's Ruby, it should work across all platforms, however, it's developed and primarily tested on Linux as well as regularly used on Mac. I've not tested it on Windows, but I'll happily accept patches for Windows compatibility. **How do I get tab-completion?** If the package for your OS/distro didn't set it up automatically, you should find that `sugarjar_completion.bash` is included in the package, and you can simply source that in your dotfiles, assuming you are using bash. **What happens now that Sapling is released?** SugarJar isn't going anywhere. This was meant to replace arc/jf, which has now been open-sourced as [Sapling](https://sapling-scm.com/), so I highly recommend taking a look at that! Sapling is a great tool and solves a variety of problems SugarJar will never be able to. However, it is a significant workflow change that won't be appropriate for all users or use-cases. Similarly there are workflows and tools that Sapling breaks. Further, we support some things Sapling does not. So worry not, SugarJar will continue to be maintained and developed. sugarjar-4.0.0/RELEASE_PROCESS.md000066400000000000000000000013321523615150300161310ustar00rootroot00000000000000# Rolling a release ## Prep the release * Update version number in `lib/sugarjar/version.rb` * Update the `CHANGELOG.md` * Create a PR, get it merged ## Tag the release * version='a.b.c' * Add a tag: `git tag -a v${version?} -m "version ${version?}" -s` * Push the tag: `git push origin --tags` ## Publish a gem * Build a gem: `gem build sugarjar.gemspec` * Push the gem: `gem push sugarjar-${version?}.gem` ## Publish GH Release Go to release, add new one. ## Publish Fedora builds See [packaging/README-fedora.md](packaging/README-fedora.md). ## Notify Debian/Ubuntu packager Ping Michel Lind ## Update Homebrew See [packaging/README-brew.md](packaging/README-brew.md). ## Notify AUR packager Ping Zeal Wierslee sugarjar-4.0.0/bin/000077500000000000000000000000001523615150300141025ustar00rootroot00000000000000sugarjar-4.0.0/bin/sj000077500000000000000000000156641523615150300144600ustar00rootroot00000000000000#!/usr/bin/env ruby # SugarJar require 'optparse' require 'mixlib/shellout' require_relative '../lib/sugarjar/commands' require_relative '../lib/sugarjar/config' require_relative '../lib/sugarjar/help' require_relative '../lib/sugarjar/log' require_relative '../lib/sugarjar/util' require_relative '../lib/sugarjar/version' SugarJar::Log.level = Logger::INFO # Don't put defaults here, put them in SugarJar::Config - otherwise # these defaults overwrite whatever is in config files. options = {} # If ENV['SUGARJAR_DEBUG'] is set, it overrides the config file, # but not the command line options, so set that one here. Also # start the logger at that level, in case we are debugging option loading # itself if ENV['SUGARJAR_LOGLEVEL'] options['log_level'] = SugarJar::Log.level = ENV['SUGARJAR_LOGLEVEL'].to_sym end options['_cli_overrides'] = {} parser = OptionParser.new do |opts| opts.banner = 'Usage: sj [] []' opts.separator '' opts.separator 'Command, args, and options, can appear in any order.' opts.separator '' opts.separator 'OPTIONS:' opts.on( '--default-forge-host HOST', 'The default host of your forge (github, gitlab, etc.) when it cannot ' + 'be determined automatically. In nearly every case SJ will ' + 'automatically determine this. However, for `smartclone`, if ' + 'you use shortnames (e.g. org/repo) that do not contain a host, this ' + 'is the default to assume. Most useful in your config file when set ' + 'to your most comomn forge host (e.g. github.com)', ) do |host| options['default_forge_host'] = host end opts.on( '--feature-prefix PREFIX', 'Bypass the config and just use PREFIX.', ) do |prefix| options['_cli_overrides']['feature_prefix'] = prefix end opts.on( '--forge-type TYPE', 'GENERALLY NOT NEEDED. In case SJ cannot detect the type of a forge ' + 'from the hostname during smartclone. Only available in CLI, not a ' + 'config file option. Forge type: github, gitlab.', ) do |type| options['_cli_overrides']['forge_type'] = type end opts.on( '--forge-user USER', 'Bypass the config and just use USER as the forge user.', ) do |user| options['_cli_overrides']['user'] = user end opts.on('-h', '--help', 'Print this help message') do puts opts exit end opts.on( '--ignore-dirty', 'Tell command that check for a dirty repo to carry on anyway. ' + '[default: false]', ) do options['ignore_dirty'] = true end opts.on( '--ignore-prerun-failure', 'Ignore preprun failure on *push commands. [default: false]', ) do options['ignore_prerun_failure'] = true end opts.on( '--log-level LEVEL', 'Set logging level (fatal, error, warning, info, debug, trace). This can ' + 'also be set via the SUGARJAR_LOGLEVEL environment variable. [default: ' + 'info]', ) do |level| options['log_level'] = level end opts.on( '--[no-]pr-autofill', 'When creating a PR, auto fill the title & description from the top ' + 'commit if we are using "gh". [default: true]', ) do |autofill| options['pr_autofill'] = autofill end opts.on( '--fork-name NAME', 'When forking a repo (in `smartclone`), fork the repo to a different ' + 'name. See the help for `smartclone` below.', ) do |val| options['fork_name'] = val end opts.on( '--[no-]pr-autostack', 'When creating a PR, if this is a subfeature, should we make it a ' + 'PR on the PR for the parent feature. If not specified, we prompt ' + 'when this happens, when true always do this, when false never do ' + 'this. Only applicable when usiing "gh" and on branch-based PRs.', ) do |autostack| options['pr_autostack'] = autostack end opts.on('--[no-]color', 'Enable color. [default: true]') do |color| options['color'] = color end opts.on('--version') do puts SugarJar::VERSION exit end opts.separator '' opts.separator 'COMMANDS:' opts.separator SugarJar::Help.summary_list opts.separator '' opts.separator "Run 'sj help ' for details on a specific command." end extra_opts = [] argv_copy = ARGV.dup # We want to allow people to pass in extra args to be passed to commands (like # `amend`), but OptionParser doesn't easily allow this. So we loop over it, # catching exceptions. begin # HOWEVER, anytime it throws an exception, for some reason, it clears # out all of ARGV, or whatever you passed to as ARGV. # # This not only prevents further parsing, but also means we lose # any non-option arguements (like the subcommand!) # # So we save a copy, and if we throw an exception, save the option that # caused it, remove that option from our copy, and then re-populate argv # with what's left. # # By doing this we not only get to parse all the options properly and # save unknown ones, but non-option arguements, which OptionParser # normally leaves in ARGV stay in ARGV. saved_argv = argv_copy.dup parser.parse!(argv_copy) rescue OptionParser::InvalidOption => e SugarJar::Log.debug("Saving unknown argument #{e.args}") extra_opts += e.args # e.args is an array, but it's only ever one arguement per exception saved_argv.delete(e.args.first) argv_copy = saved_argv.dup SugarJar::Log.debug( "Continuing option parsing with remaining ARGV: #{argv_copy}", ) retry end options = SugarJar::Config.config.merge(options) SugarJar::Log.level = options['log_level'].to_sym if options['log_level'] subcommand = argv_copy.reject { |x| x.start_with?('-') }.first if ARGV.empty? || !subcommand puts parser exit end SugarJar::Log.debug("Final config: #{options}") # if the command is help, we don't bother to create the Commands obj if subcommand == 'help' help_target = argv_copy.reject { |x| x.start_with?('-') }[1] if help_target help_text = SugarJar::Help.command_help(help_target) if help_text puts help_text else SugarJar::Log.fatal("No such subcommand: #{help_target}") exit 1 end else puts parser end exit end sj = SugarJar::Commands.new(options) valid_commands = sj.public_methods - Object.public_methods is_valid_command = valid_commands.include?(subcommand.to_sym) # We can't do .delete(subcommand) because someone could, for example # have a branch called 'co' and then do 'sj co co' - which will then # remove _all_ instances of 'co'. So find the first instance and remove # that. argv_copy.delete_at(argv_copy.find_index(subcommand)) SugarJar::Log.debug("subcommand is #{subcommand}") # Extra options we got, plus any left over arguements are what we # pass to Commands so they can be passed to git as necessary extra_opts += argv_copy SugarJar::Log.debug("extra unknown options: #{extra_opts}") extra_opts = [options] if subcommand == 'debuginfo' unless is_valid_command SugarJar::Log.fatal("No such subcommand: #{subcommand}") exit 1 end SugarJar::Log.debug( "running #{subcommand}; extra opts: #{extra_opts.join(', ')}", ) sj.send(subcommand.to_sym, *extra_opts) sugarjar-4.0.0/examples/000077500000000000000000000000001523615150300151505ustar00rootroot00000000000000sugarjar-4.0.0/examples/sample_config.yaml000066400000000000000000000016541523615150300206500ustar00rootroot00000000000000# This is a sample SugarJar config # # SugarJar will look for this config in: # # - /etc/sugarjar/config.yaml # - ~/.config/sugarjar/config.yaml # # The latter will overwrite anything in the former. # # NOTE: This file does NOT document ALL options since any command-line option # to SugarJar is a valid configuration in this file, so see `sj help` for full # details. # Autofill in my PRs from my commit message (default: true) pr_autofile: true # Auto stack PRs when subfeatures are detected (default is `nil`, which prompts, # but use `true` or `false` to force an option without prompting) pr_autostack: true # Don't warn about deprecated config file options if they are in this # list ignore_deprecated_options: [ 'gh_cli' ] host_configs: default: use_forks: true github.com: user: c00ldude gitlab.com: user: thisc00ldude gitlab.company.com: user: tsmith use_forks: false feature_prefix: "tsmith/" sugarjar-4.0.0/examples/sample_repoconfig.yaml000066400000000000000000000065111523615150300215330ustar00rootroot00000000000000# This is a sample `repoconfig` for SugarJar # # Configs should be named `.sugarjar.yaml` and placed in the root # of your repository. # # `include_from` is a meta config wich will read from an additional # configuration file and merge anything from the file onto whatever is in the # primary file. This is helpful to have a repo configuration that applies to # all/most developers, but allow individual developers to add to over overwrite # specific configurations for themselves. If the file does not exist, this # configuration is ignored. include_from: .sugarjar_local.yaml # `overwrite_from` is a meta config which works much like `include_from`, # except that if the file is found, everything else in this configuration file # will be ignored and the configuration will be entirely read from the # referenced file. If the file does not exist, this configuration is ignored. overwrite_from: .sugarjar_local_overwrite.yaml # `release_branches` tells SugarJar several things: # 1. These branches should not be repead when running `bclean`/`bcleanall`. # 2. When a feature-branch is made from a release branch (e.g. `2.x-branch`), # it will actually track the release branch's upstream # (e.g. `upstream/2.x-bramch`), allowing it to work the same as as a # feature made off of main. release_branches: - 2.x-branch - 3.x-branch # `lint` is a list of scripts to run when `sj lint` is executed (or, if # configured, to run on `sj spush`/`sj fpush` - see `on_push` below). # Regardless of where `sj` is run from, these scripts will be run from the root # of the repo. If a slash is detected in the first 'word' of the command, it # is assumed it is a relative path and `sj` will check that the file exists. lint: # can be a string which is just a command to run... - scripts/run_rubocop.sh # want it to have a pretty name? Entries can be a hash instead... - name: mdl command: scripts/run_mdl.sh # `unit` is a list of scripts to run when `sj unit` is executed (or, if # configured to run on `sj spush`/`sj fpush`- see `on_push` below). Regardless # of where `sj` is run from, these scripts will be run from the root of the # repo. If a slash is detected in the first 'word' of the command, it is # assumed it is a relative path and `sj` will check that the file exists. unit: # like lint, supports bare strings... - bundle exec rspec # or hashes... - name: unittests command: scripts/run_tests.sh # `lint_list_cmd` is like `lint`, except it's a command to run which will # determine the proper lints to run and return them, one per line. This is # useful, for example, when you want to only run lints relevant to the changed # files. lint_list_cmd: scripts/determine_linters.sh # `unit_list_cmd` is like `unit`, except it's a command to run which will # determine the proper units to run and return them, one per line. This is # useful, for example, when you want to only run tests relevant to the changed # files. unit_list_cmd: scripts/determine_tests.sh # `on_push` determines what checks should be run when pushing a repo. Valid # options are `lint` and/or `unit` (or nothing, of course). on_push: [lint] # or [lint, unit] # `commit_template` points to a file to set the git `commit.template` config # to. This is really useful for ensuring that everyone has the same # template configured. commit_template: .git_commit_template.txt sugarjar-4.0.0/extras/000077500000000000000000000000001523615150300146405ustar00rootroot00000000000000sugarjar-4.0.0/extras/sugarjar_completion.bash000066400000000000000000000027071523615150300215540ustar00rootroot00000000000000# bash completion for sugarjar SJCONFIG="$HOME/.config/sugarjar/config.yaml" _sugarjar_completions() { if [ "${#COMP_WORDS[@]}" -eq 2 ]; then return fi local -a suggestions # grap any and all feature_prefixes so that we can let the user ignore that # part. If we have `yq` we'll use it as that's going to be always 100% # reliable, but if we don't, do our best with shell utils local prefixes='' if [ -e "$SJCONFIG" ]; then if type yq &>/dev/null; then # We don't use the '// empty' syntax here as thats only in # very new yq, so use the more standard '| select(. != null)' yq_search='[.host_configs[].feature_prefix | select(. != null)] | unique[]' prefixes=$(yq -r "$yq_search" $SJCONFIG | xargs) else prefixes=$( grep feature_prefix $SJCONFIG | cut -f2 -d: | sort -u | xargs ) fi fi case "${COMP_WORDS[1]}" in co|checkout|bclean) local branches=$(git branch | sed -e 's/* //g' | xargs) if [ -n "$prefixes" ]; then regex=$(printf '%s\n' $prefixes | paste -sd'|') local branches=$(echo $branches | sed -E "s!($regex)!!g") fi suggestions=($(compgen -W "$branches" -- "${COMP_WORDS[2]}")) COMPREPLY=("${suggestions[@]}") ;; *) return esac } complete -F _sugarjar_completions sj sugarjar-4.0.0/images/000077500000000000000000000000001523615150300145775ustar00rootroot00000000000000sugarjar-4.0.0/images/bclean.png000066400000000000000000000032641523615150300165360ustar00rootroot00000000000000PNG  IHDR+ lEsRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME }- IDATxm* Q &;X}*kF1|m۾ `+ ׾ 4D_=ƵzBmN΂gٿHEJ1ϩ:F{-q.]/[MKeuy򒕫1J8^^sF!Y۷Η'A+'kB=(#]A)*/-_ɘEGZg>RuNhK6Z)4Eh{Vz>sKrNU~[_VyZU{_ \% Wy9!O=חP҄mJk#~Jtsl1PVO҃uo#XYx`=i_9g~`e-k|~!]Iߋo&(?:uU2*(+מ75X O>xLk扩LR*9 f/KgGȏioPG^x)LMKM*DDQRqrtc>6:iS=e"" zH-׭/ ]:Ba\se])9i{\JVy-?{{ʏoV+Izƞ$~n-ؒg g<ѥzOwjuJ,$tHXBI42.#lfኮ(x<~@u{Ä ;F-bIɲYttm۾ۿT7mS^OS}V:ܱuk|=~z=[>sϷZ\zcb؎Y#%"+Rl;q?Ԣ(}xx{E#oY+~k%4b|dճYb+H 9~JC[>e"&b=)"#%xBjdJ |w*p}j1K&_|Z=/v֖F|L[SJ/sYۗ·;4|WpE*\%_k119~Jx go4Dϥ4ꏷ"2W-$''~Y7c.ui'2xRIroMQ北cm{ǫ?5C>?{[N&P$/5G8gik1kRJB'2`vISUjc`odaGE=ּ rݡJ+ bnaVA^đ}IENDB`sugarjar-4.0.0/images/bcleanall.png000066400000000000000000000047771523615150300172410ustar00rootroot00000000000000PNG  IHDRT%RsRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME  <:6u UIDATxa* F>#Zt $z9"J) !|}Xo9.5Açg'|i/OGf;׆ڲl%^}G7zxԈn^8i\ap^?Rw>6`aot[־tԶٺ:ZDAizgg{Գ H5<[z p/|b%3y|MWߤX@J]3j -[5 -,Rʂ)S'r-Dg#MN&HgFH)?Aːe4m<@$Jһzv¢,L=W\vu  j9ٺY^ՎT!o`1! )=(dQ<>Sobԋkx| |G dw)g`ѵ-Qk]췎 =z\,|Ġ$"X.Q#Eig XH#Z`-;#j> Fڟ(ț*Vj xٱ7c-<~#Y-捖}|yg@݅QClQ<>ox:z;y|^OP գAgZvXU=2N)mRV|R,IE[yRde-_zs*j$D:5buJo$h^4fȬ:GD+XilQmt$e^A욼w"xjLJQKvJxFKmJ)7¼;,ò!FސJkz|K)rã}#;g w⟐efwi@n[mK9@y4gV=v,^%woS9 cV~!Gֿ֞Q$ǣ雞.'ZaHX댋s`i=+y~#lEK%^0j4`-5ї,82L3b/?Bind]^V.k@HKO랩-c'Ʋd*?j}£v`#f$3٢yol$F;q:|8g wRSE31ȍYo㼟̗"ȝG 3Ȩ1hnmiÈv׮mMtVܜXDnRwڗ^(\:s=8xyQ5qG=[Qڰ$N p՝,jjRݞ׆&.wAHM@_4 |8N NIENDB`sugarjar-4.0.0/images/feature.png000066400000000000000000000040221523615150300167360ustar00rootroot00000000000000PNG  IHDR,h>sRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME hIDATx]Q*fG%dMyro%A019k3F RBA/Y@AA >g܅޷i'l0 3+]_;^?Wwk[#FoM>U|t ^ 0;n1`zB/umk{kF{M+ި!"+y(f{WyOVP=ϺJF1;r`u*2 p=ٹ~Q//[7DzXH"j!-=# W[Zߑ=1D^ ,?(!{5g"ET-_UwK CoG(>!fկPZKGCU$&Pfգx29(yQQ|)rw}v#CCDL]ef9| _YrhG3+~Xd1;j=JV+3BER?BNrZD7G|TF;Y-#{hhݣ=Wz^ףۿMX&SHڮTz=Zet!mZy@D>F9^>Am!-g=_YpKXOjLؐ KȊ j.g;P9eC$|'IY%[ ;4ADmA O W[w1^}{x738T~T Ͷ XF4Rd--`<)֫VhQ G]э)=PB'Z3ɋzk݄F/g'H"GOw7oOt\9nxǾϟuvVFZMDo&#/ݏcgm۔[~o`xiq=K~K1.?`X=׈S_ŋw ʳ$C,'H{۽䁸/I:{sWL;~7 聊y'4M%ÚF#;L丙i.{R6;M,П0.W4DsdiHތ&ծVIf^h~OJV~*@W\&/Ot\9vGЈBO_Oy ? @-Jϳ$$2@W?PYyNIxTiқAvϏXKIoFd-3·'@SgTĭm7s|kom hGiA˯FYN,۟i"V++g]Q~9V;:4~ĭ 9oMI5Z{]IHK^/J'R·d3r-3_Vy T [2]x=Wף^liάD,AձBaQd#* V#-YGzA,agHGڲ9=h.kjZ=|( `F'c2\= Ɋ/NNNXd)Y#Y}eNK횯Ht-Tԓ%0}PN,.# |eE>Z=eYwxy70PSN]8{ҾPLVA逌L&5mM]OlڱkZpD_9Jv$I6([X !=R[gLe=m-Mz; ȵ&;@*iGO_kiY]WaG=9Ϭ;|rxd k{ Wx2|{5QJ76uc/N&-$k" lw9(8:yZŖQ߈$# 0,QZu#EIF$`ʊRbvzF$D āH2@9;H\J#&1r+^q]ϊuE="-.܎E%켇U`նKy7(+;`6X'6v]m'X>41@REM_>O6O=i,#`0W[/(oˊ&Z]*)jնڽE :+/ViϧXw7kK[fD=!=籗b^w+K[kD 8Qj,5JY?Ymeb/iWv[ D)YlK<^'_][dkgf_qcG+cm񫖶Xe-iNiD[I[ZdxkINu-GݽCgׯz^{~?[s agrTͯ'23-8MoP\%h 3kX[^ˈwU_M 5QF&ډXKB=z{ҐdkRc߮U2i+C9-zJGRՄ̃)Z,S5Q}*6KN; "4lOhuG$n$kEIX|VӰZHYQ7oD @ Q(Hc篎IENDB`sugarjar-4.0.0/images/smartlog.png000066400000000000000000000111151523615150300171340ustar00rootroot00000000000000PNG  IHDRliCCPICC profile(}=H@ߦTD ␡:Yq*BZu04$).kŪ "%~Zxpw}wШ046ɄͭWDЏ!YƜ$;]g9zռŀH< & ޴ QVUs1.Hu7E53y(X`YԈcSXYXuHbK BA eT`#NN4'|C_"B29PZ /)B/1wfqy+6Om-vmmM.w'C6eW  ) =k^Z8}2ԫ pp){ݝ}տrpSac pHYs~tIME  AUtEXtCommentCreated with GIMPW7IDATxkqWv4_B_sBŖ9ms?݅sӥ;϶G?,"2JI7/+}9,Tx)p8v,z^+DJ J,MǡƊޮ#/i7јٯ+͟[uy|mf|LVsS)^4*'jϰX#8?~;הO/M}$6qKW'[ X-FP: Ls!x|5 l|aLXSׇ^qhQnP,y87B[+JOIeGMF^ tl6Q#.|j뤑^ι`NklF[a6c.M#d'S&-%&GV6~>~s. 3]s*M?ÔJrDkJc`GPW^Rfu@nd( E'ӿlopڛ9:͖&~ӣa"Te].YN\l〞DߢL>i6:}s6Aǩ@xûPwNEk)f/`ɦw}`'z[kW ͳd%FlG5}-H䃅s"f7ĉZ$)e#a8*sDVs׌VJsآBE?[CN> {b!`G ߴ·jQg}>+0?_? 9[:vγ z ɤ+=q^>}Kk:瞃uȄL.MMzZ.[:[7 MaKeYS-@|~yиw+XH1M:!"ҝ]ỵ>~Q^fE5Y6P]\yM+-XF|8ӻEa[L^n+$qStG>zg3CֵkťZbQ2_jPOæ}ą;t#0Xҡ?Tv'q I/xnKGrFo wn`5M}45d[m>,~|w"Ih 74Z*[.,>_@uuf 5 gjhrp`uqo}N`pEjpk~ܗt҇x?Q@ 0*Ǝ_ 7MK·cQ;NRsGsfeG'O?r D xs,M".G?G{ϞVWQ:(qWCa)| CTYȿQ|Lt9YG͠^qn^$rx"o]4Me wE;$j#dhD|5oM2l<5_d-w3mǻGgrIIW:kvqd*F+lWE=|iF]/h4?keqhМlS6h׊eG5/S*jy&Myц&JmrPxX] z=twa6Q4j6VcW#U}r$g>=-^}NYXV HvҗբTOҥ'6] l#z4;M_'hV:u *2g}MCZ#/iÚ+b,|L|I[riʌb>z}x=J"G%TqqLo"1{9i w{0i:'T׬᛭QX7h;kx^1M;@ 9R6MeۗA)$ϙb&÷)".zFee*^T8ᡱsYu[iL}!IPF.vr%v78Fy87iqlu{\:cPr>Ṙ~rz[ sѺ{< e5iRB~L0 *[C[qS.•̉NM+.zkȀ*%2 CFM6[֭v4Xƿc7@dQ&Ul~OmEO[R19٢l-z#eT 8}j6Q7s2fVDA'Ly.[zؼ`iǍ87zUÂSW\(( ~S ^ mIxϕ.ސ[D_ 9 vY40nCL)M跥3Hu%ؠ$37 5]qI5J est]@+m`g9kF \ 'Sgڠĭxri9LظhMʹNJ_{C_Ui?뽒k(LfLVmsW3&)ä8LnWcy Ikuo\mKvweF@o=z$--SK=g-@-U,*&E^#j2;fN>B4BBLD3<,Rׇ8ZN P _p(&C5n GaF?̧>vi8+ x\\@ވ?G>7+3JԑCT q09U7o*c6rոIENDB`sugarjar-4.0.0/images/subfeature-detect-missing-base.png000066400000000000000000000046131523615150300233030ustar00rootroot00000000000000PNG  IHDR;;sRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME *HIDATxi#! FQ:SdXl 'E]C0mg*/T;et>;v8|>ϏȬiӎP2SȍwU}^^k>`}/=qrz)V00g9c1ΊGfsyLrv#e IZ}2f08zpcw Ի=vINB8f_ﬨan #s[O5c)ߒ>T87M_jV[jr5Y*WN-{WO?ڧ{[{rYj?Ns_:juR,zϸr?[ܭ_EyQ=WN?2{~r&=::]e0Wm1^{)L9zy:<3V%npg [ݺTS$GÌp0xW9;l3f}0W\RY87/~?Q0kV6ҁFV VsKZJomM 0g\:bћ^5yMuKm`m󬺰a3a0{-l=V9jCɷs&2DI~m{{]ZQkGW3mVj=+GZ,ܬzE)[[֧"*lTTd8älamȯDw& 4}޷^z˖,tApڑz^p?%v .nw4ȱ^^9 ⪠|Kߧ zo*ȷ2檢]jAi^nxh^=b-:K;LU 92GrDG .~}ފ"d lǻg^ /yEiݕaFo+E ^2H9#Ϟ(%[QdFz•s~fع7ӶiO3Q-l<[ UU絵WZݍ;X9>T`dW)LA= $m -L|[oyt;>W; eRG6g\<" qm_^b`AӽL],[.#+Kr=.D]3WTRzH 4Lߺ|3 vUa<" 9}Qi"}X擝~2GFk`K67*8#/Оq{1ҿ7'gO曦 aM~M^<8*s|Z% g۶϶7]|e^wgϗsr{X('W/=KRˇ-ҏnmO;;Qg`N;ʻzoX@ܖBM] ; b pE~罱S/⼏=iG_qAk0߅:}nj7C^9x}'ߙGѩ:ZPV0oKkrrLWzCyO;X} kYQn(Z+L(P:`u/^i 4f+33oN1k]uuFcuǯX;F橝Z^Ҳ;o_.;@M9ˆÌs7){/[`q^훳yV.hŲ%qm#,U+=F_AocA:1߃iYڒJړh VjҁFV VsKZJomz/(YHc=ѣ7jj ҟKS*ύyy wKr:/ug=z\h_9e @]Z]yHzE9-S{WNnBYnv mu=7%-e;.?)p!ng q\&B8L&Dl\ IENDB`sugarjar-4.0.0/images/subfeature-part2-smartlog.png000066400000000000000000000124601523615150300223310ustar00rootroot00000000000000PNG  IHDRAsRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME  fXC#IDATxk*5';/2UP~_'=6K oۖ6x%x"Rwn_ooOekRJ۾ܚ7E;+[.J!pz,[;_[#[跧)+6.g>]K0< #-S VN5)#m"RF zJF`a\25yMc< ^rN'm\{-k5kSψjي4pRwXR)ux<3̘x" ҠTj^PksS`{\\9:*g]0;N1x/=cQ?[`k4ep"pK|-|̚U2U[uGQx~OүzG瓯not뿅QoUcuU>WCd>Ou8YYZ6bSh{3 i ox˛>J*r4~֙:<iQVF"꽎odžBtվw3-j1\x2QSBroE޻ǎ')\>mCpPma}X~_r<_9ʿ+\ڮ(7_ ho,F^ n^R?q||rz;rPn4=<1zGL֟p!ꟻ<@G&@{k>[~_D%s 7=Ak_x!@:<Sm<ӹK ߵj6:L׳9OW;AYe}Bt5U)-VˠOmC8IЏm`j7HѨf R4KأDHPe"Q<# R#x=ZX VP\ҏk :R_{nZY{=+}/ O>Rz (&@e&k.|OlAR`=,GʍQK~\>G} (fj{GSv%Y-՛>-)({YkOT'@ʣ57wvСӸ7@OkdE NP6H#=A\SHiB}>cH%WTr @f#`yVEVbٶr-V78^x9ϞK&9^j/GZZ>QBNDTީNM ^@X_pA\n-!PoߩiAZ\DBpykGӔ#Yvǿ9uoqy!v= :TrzС|cЦ B7ż 9UAJe]{7 `4[9Ҽ ArxGPk~qN%|ZIq8>#W@6=A0D**|zHiIAjBݓZw@Nsx0 'H5v£xG.Qnee_[WS&JŢ5?uԧC],XPZuMAX@0n<":@(]eW `qE^ x"rPbE [ <("&>[)mmse,VGf__s*)rʀ])IoFƚ]/ZoHkS<5鷣v+֭}]ެGϒ}x=5?U[e/!Y˽R.XW#fgсW{4~wDLYKmt@r[;?5=q5%mjT)-Of+9> @E|ZbiqspX5.^!4p^K o?KEPS[[d濵3I[s-fxz?#VxM{Wqs*boRg9Rpј)}ģ@fyVZ}P"GK/'Lg,D~w7`TnҼeJ% ua};pF@;'x[߭yML|+ V}TB³<¾o|[ʵބ'F"ܭ""`x@(@uVE@K/UP^*x^saaGEP^ʇ x/{m__H#<{)Gh}2< RWpuq)|~n5RJ?[kzB^v mV̒ЏH%᝚qMivAHr^ni|]*tmk}wήg~IPIe,o Y飬}[KXoI(7o\0VΗSfhu?27*ľ{%*\ϖ暶{˽{lmo;Gkϔ i*!?X;[D\:Jntk#6.#&fE&=VxMј\+X|jyE)oΒ`x* [p$GXs}z:u}XKw]/)>- xѣ(^`dLL!1ro-#.Ooƫk6-k ;Ko '-ʕ^ ՄWyX,n?vN/+h o,I ʲ]3]볮,Ũ,+|wNz!u bt=\Bm:R赩3_.,k`ް4Z}9-V贠(m|\KޭFdO½2ݷvCk8[̴ %tjnl"E]zsKY=g{h_gӕNq'kpm?C!\NhUNVjw:'>HG<xN/1ހC(tz_4?_K_gOZP p?"} (@[pEA۪7${zO`Z8Y28].>)wjRgQ]X 8\a]-E^/I,Ⱥk{ P2&yzfElQssŵ 3g *`0:" `UFޤʇNl7':VەoYtծA)pVuk}F" Ԯ)=Vf &i5|2 $sU>R>PvX5%4"6DL 8iAf?Ol\2E?&ͅ[~ Ó6F/QܚgD{5l:ҵ~e9KQ'T nH%xNNw>`opQb);m "^phʞF)ֺg,9p#E ̲#ʲ4s.4g5Xׄg@iUlJj =ʇRfUMDT+y=HEZ-H:ǬC#؈5ZNy3j F=]PN[g}K_Qʕ%U4T`G5mnEZAlz\#AFk]wWf[}5>TڂZO0'^6ķ,րe2ĺj;mAx}`LPNw.O_e<ִgsSY>EYϝUk}y}=wޭ~>ewn#,^ȞUi3?}Kzْ~~ #e)oh{~Uxl]vW%d{X̺J; rUdYrboաvjQt#zh+GX{nMr*=<~6뵯wWwdkj g5?| :fy-ڏB|vc},NwQ\Iݟ??>ӝӷ5s9k,|[kQH>G2X˩CZ>K?rS?ГsYY; k o(G{LϑP>ލ뺳ϼȶKoі!z..[^ 0#HA$8rK {R޹ 7$~TtEp+;Vu*p}rh6o|V|F_9 w[ z~dVKY=B%yV9D'Z|Z*S)>ڏ+ExT ]\ a؏[#rj3w>X |W,{"P{ jeēofW=o~]Z,qM/W窺KSGڊк)߽܅?WvIeOGˮY٣ZM%9w[f^-'W>_̷%IQƚWAH>w cww>VGJ k; L<n.SZ^vYf0%woDe(X#,u :)Weʀ)r9[1'-=gzޕ'"+VVwJU2gOVH_ex jv+W+Y--gCٛ]TP\rG`ֲtKp3݃29#Z6 R4:ox( U)fOCz畃?)to'-(|q+FhWqƏQ?F`ψfJ5#s-AT-NFm[ODzIzowF^qΞ-ye`'Gcv"L}Ҍ5IU"*xESP:{Dȴv$~.[[8|T`MWsTJgrzwxmT.v'LoodMuNu@5#.#C3#@W_xECy"p-Y~3p7yNq]wy|y(U[>_ߧ`{v)JT*;#"KAJ]CwV?rGСEС :~l烠C1htTUNo#yx5(S$ ()A!ATG 3A*q,Ek:~5ՓV>޾dg%{rn:·C}GС~=th%7snTow#DGСEСgB!ر]VAh :A:AZ^)ӕ*+37+ Ar\݃tf׎Srz, %P,Ol0@[[Cn}g ̠~iPQOZ}G^j/QDN~);յv#"Jf <]M) !*LT皠:vAB@T6[D<.}j9n'H+5aֳ72Z뤘$`sG:וy5yFˀ ÂՉrٻJRޫ:l"@PAu"AKJ|2V.ݠCY>gpD@䔿7xAcA/UpZ^8;3@k#0z L&8ș,gEy""*tz_7FEYp4>A,zi 4e?LpPɐ{fgޠ= E'hO+UM}ܺEm# 32xւX2A)1X5 D Ռ}62ܟm RdI9^u\ɨAeuX8 (`kQJ{]}'HФađ5pb<=yA3|b 8p ~3U2.,vxE00W;˥;sRM?\T)m= ZKO۶*yu9X/ڽ-gCs5}3Af±1zj/ 7?zV'[*g\-ӝNRG c9s\|w6U M6:W+%()'ȌY 9-mZ1zr7P+e*yOv6ը})r-O&y\Rd- ^<{ C^[lCxSoυN#G 2Wurޢ%-s+:4bAT";5pj>+ 9{s4p%y|J߫Uϡ(U,AJiJiTrPvxˡRK~.[[8 `Yy^"j4b{\MnL_|.jkg 7mLwzҙlV N[m#!G"x^kG Ye~1I`GR-߂#`"Au`x^˚Z͎ ӊ7 x#(C`ޠ:|T+ ^)a]I6[8$>ZPОv\O< `~?[JSǚfsD>k-5>G|JӫZ>KKM=g6>z>7 z>zg2#[wz{'C=)Lpp꬟WݾJyz<{VH?_(Xmkk`"A0b8RtlfiqxU|DNu*x^lf W hP8@{$<ד^dl n+ļ2BvWyPh}?2+%NTuݙ)ҫkɫ5K|۫.V=JZ^lߨSV˧UR9rXY%F`Yb"`5On_O +Б8ҵTqG˧wV|n"wk岼fkOOz\%u4kf}ʎ쀷<I#R!K'{{zDޥ|Νsm2JJu1̙)V`ه]Q>Oo-Ot'S.qg@ՌΆo__Iz5r}>}EQ꣞^lM(#*Pي:2 fΥv,˻EL9?4"-:V`j/wڲ#ڒ3`ɿX ,.~VґG;z>G[N?20zRS|-,Zz𲖳'Yg;nwG.Ɨ<@Vʘg 42^6$J=3}K*Wu޼JfNfVo9qǢgT"ek!fͪ=XBTfZUgjXgpmwcU'WQSR,"c@?:(m!yMr,AJiZҩ-`MrVZ;f? Pہ"zE. &{{'[3ϭ-}zQ>W=ײgҫrZ NZP*G>~j)2Ne5Jmgdz ˒wET^̷ 2: 8@۲'waoщ6kE| {1qp@tG1 =l<y(́G+#Bp0akG:On_O R^'ʮ9ψ)"߬Ψo];~]« ޽z+v79P;ZުJu㓯b1}okߙ4G kˬ{Wv$՛Hd\WԬjv|={rzY%ڲYKkT_֞[~3UEٮp3s;UےO2G2z[Ik+-$okMk[k߬RoB:#PjϞ~t X)]p&:@X.vޞ﹅cRS|-իTƑNXӹe%kTطQyZ'F_ zƮܑr^j&5G2(Rz._jrxryê+}_toE\83`V'Ϯ=8"7Uxq”ɝۥo=cU'KCtlѓCGW%2 T^|m #/ozsu^:iğS%[Y]@*]zsv^HX.M[{{kJQz5Mx[["+k=]k/.-Xj$網8*|5ݨEkΧV*im g=j,R{=Y) X~{^̷ 2:8@۲'FG(8l r"N@|-/NLdMѷJA xe9+U6{3Hd=opZX߫f#Ajwܩ^G : gvB S(k?{\,%op`J`PykpAV9@<}]1l9 ڭfM)7-{Z}&$9wD)(WzG`G"xɮl)ԩIENDB`sugarjar-4.0.0/images/subfeature-part3-rebase.png000066400000000000000000000221331523615150300217410ustar00rootroot00000000000000PNG  IHDRB̌sRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME  IDATxQr( @Tn `75Cc Ѿm۱+BvC8? vVY~xv#pǶ!צM{ׁšgdwi*;KK(ZKy3bF#(4Kou_KFD_#kluG|,[zy'lQm\ CǃԣP?ҭ"U&Fs4{?O܏cW#vlLY좵nƹ#CZBњ>M6жWdiZ>@taȤKz͖u\JU[/ WFY͖{I,Qk=gMEbXgKrm^+[)P5h5D5Դgɳ}5yYq:Ph5wDlrZQ+gm/o;>+JdC#wޞUO9t6}C_؍zQekSJ^O]<|]? j π`)=Wz$:C 'һr%e-Om#B>VEy`P@@d|VFKy4U|-,`)U@i4JVۮ1RgVK`C`fwi-g<ފ***F9}K5JS{<%f uYm^-aהwp$, ;6m1 õO@G{P8l<C7/;l~1V&E#]9@z:9=P.FӪ@od;s\qK}=%/A{_ܬ-k[` J0`;٥VOc'Rj 2Yͧa%XS)^p0^ ku%Y\63wC^Eɂlo&4Z\CXD֋+^}v_c`<ݙFo :wpĽXiS7x0,P@4ŕ;\\^ΝamKid{Y_{v߈f?Z#Kzvʳ|oq#H5IDh *!C1[^D4-A[Ia B[ޠR֠FvomO}MYǽ!P f/ip^ٝi-[>EZ߻CT0 ]]W!pmA`@9HCnIg+'y̸_ Tנ"ՂׂFm'ko=V ;قIq/0&X])Y҆MˌTGQANeJ,x۫5GZf ]:5jiw,o@Zx`vzZyWq\˩vݢɣ\L\|ZBXmbTyb~{pZ=wo\Dx@YE4+kufN]ϹOd٥{7_֌Y!`̞f+yH #L~Z{= }Eav0+ߚԙs~;;%zLևz_IrFWMtWZZS8C<ϩF<9XsQҀ\3uxK4{x|&xXDl=XAF-*sQ{NkJFWGzX8on5ruE#=C*|}K򫕧0y:;I߹A"@2nvlOO;_cosrz._O>_ի6Ore?<|sQ81'< n標5?)Jub%<5~i`2E}}-'i).B7b,I~TfQ?%WyViZmYw_xR~41A*3RMޛO}?ǒXhhh\KAN4dJ[{ik6Ö5t6{B"P~xGaxqYI:3GY–[ÝZONjh(ٰ=woܓLrlLp{z*r|Ώ*z_gJp\?=[~".ܽ^Gk!pp̺AWPr^&O?Mӯdi+9ʷ(ufmpŬ RZb{x,}龒<匨<\-\dJyF `ǻq.jȐ,r25riJ;1O+ my5ص5>mCV}%׳g9MTo Sh-(&U*W)P5+h5D2lɁBfK߅<#6ЯzMSnQ"۹Da/5P\s0~ yk87|!m|h]׹Aki4{it[2m6Jӕp jS临Hoէ=Ƿ40h܀tߥ[kN&W7??()+S})ZVs({UZ/CVof1hK#`==E/-%Z;p$VfGF-[{h)g.Ir-AjAxhYh-' 0rYfϒ> i)k{ ֏d h{=sݓVv[˹)Rһ598IkRUhr CbfܩB6h \OG@rKi\ДlQH[2,,3܈nAiJl?5lf͖9 WA,e-UN#/`!^_C0^' L A6Nz;:*ڃvjXO:~v`8 &MA3O)?F(}`‚yB I}3B0XfJK`GYyl#<8N7oR`XE*\*O pAir<dztC:ӲbлOo4iߴ>:·>| :kצ#OsСnLkYJ=!Ms25P:9H:(A.!;֠70,`UԹ~$Bph!w]zBi52IҦoY%Vt~ h#,*nfR>6`m8bC0@Ek3cue~RFoz"h^8}Z:s"D7@߭ҵXyb R$)rͳ^ހdٿ-^)< %fAN7+܌z}~nIrCG" ڟR P̌SmHBP4J=Ir߽`P C_5W|`P%!w@/#tҖ6}}ގ\4Ogo KBX{6Bm/m[;1:gszoDIeQUaG.]Du@Yb^O .}w{i]ZIuR^rT< |k3y)})ܦ9lx2wy!:㋑/c A,})WY+(?t}<:бIɝt8 AT7cYho];xr*uk멄6AZg,:#Tژ-Oi^[ʰC5_@i_7Pn殝k)ֲ0' C *P)H@7D9z wT"?2KnD! u_Ӕ<$hwH)zy˟zG! jXe>j^w+A ;G@61(=lP? (P$BrH REͧˌ6~IΊK,xVW[10[ !B A `C0|mc37K x!7ॆF=BCo x.x1x0CT|>=ַ}7+M>isOqi`c@S{Ko۵m[ߙ ']>>OwiZKn_pz]ۺџUzi>om/>|q/ cVq3YlYހd< !˻q}f2ց$߫`K=Bw?7~߮ 1Rm}K;-k#-mɕ3rDRކU>#g) =UΚYwI羟˿ 3ץ{AgjJbzfj}WSNK޳ז=v3@!`K ߪ6l|p'5 n3s}ߛ(pGymUVRy0"#]/3&1"r?b ;ЪXfͭ2ӼK{f~ 8gW}ܾp~x#*uQ Xɵ-6r{zl2 x=r,}gu-4ko \yR9#Ge_[&ZseFo“^#|k֙|V7``XCA0PO:!dK /50^x^lsaGC0^ʇ x/ŽGٶzG>:w4OO>rYaGE}?rے$wߣr 2>yGKf~6q*3WS='=ٜb)|4Qi_=$ zLSߜܖ.E/rj[YnY)aS|?(ovQG͒m.e5܎}䶬Vi0VRK)ntub̲6vfVv|rY3\7mra7GO5(R9sLْ'悶1F-zw5i<5viY6QKBKF/=us^rQΎģV}*C1ߡVoq䣕޻z :~ֽ+ӿWsS3 ""GD!m-{zz*VErkj[2t@o^EQhOoW:[ynßx&QOrM$D2 n+`/f3׷kW2{o ׍~^/yos_Bnjϑ{7fۆĥҌ^Z њxBַ[8Orkq2nLkb^iԞtY*[G&Z𣏕jrk|J-3tߖUᑃi9&3)}B)RznT/k}sܴ񖳷x#V gmvV*,&Spn7r;OCt'CSZkuJsa뫟8CɃ2 /b M>tGIDAT%y w!MPm>EKK((7k#Po0 QrC:!-&Ȇ_T gk}#{ګ,=3038^Q.Y]Q2IOϪGs_)Z}]h  y;[)EjmwG+x<V5[#TXgvsF@'gm6-j7zDBU[̭A?h4Ϝ %G 2s7"hJ;sw=@;=oߌ{GizkSzl뵞l}?~`cDy-2YKӞy}ϲܽR/^isFT)J'AFd,GM=)t :4bǬ(ʊΕ;3^\kA{,A/ \,2ݷ4 3ҬIjZ}܁H# j<`,nmIw}Kk<5dm!O}KJȪ'4W"Ԉp#]rR=fQ[RΑXyqTyF-{ư+}.4*g zzRtOܟ1}tk`b+}OrԆe.y5uwpM")mӾMc5}Q ZKSzozNPx]Ӛu6is_DMT`A9egݤe{K_Y,ђ6Znp,r_4! mt~mrQ&#&Q(7z5ӫhC"k IENDB`sugarjar-4.0.0/images/subfeature-part3.png000066400000000000000000000237151523615150300205110ustar00rootroot00000000000000PNG  IHDR0sRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME   IDATxQ(@uLޏYϸi{S$1|> `y~@fwVYʻ[a<Xs#p8CM5EOԷ.J]zҿs[wr{Aya?Gvk.(qusJt6/#V%sP c'_Pޝ-Uu {e84{$n}]:6-h[-[Q_iv6[ e=a1W.iIksdgȧGɣEjь}qZ\M-}')=^u;wT}5ʵU Zrˀ;}ˀj]bZ=cQ4$XcϯFn4q8<(@v^ޞo[f G}g菫oc vŅ]V*`R_A `F[5OmxKaFn,k5t N;N_,`H38拉Kz'"%6.oK 7w#,; @T*L 5˸B2 `k]2#NYeyP,ە]yo<.wrJyԷt)溷}#xo{i,a9[k幋6MFҙ$  .`5Z1s0wRAJ]쑿UΖ EvEר5&U^{`"@`'Y=GWρw`<3C`:f cy_ ;r Vڱ, x9f1:[Dlc<i}l`>*Z)_w/ȷ]}~FwYCCK yT:O0>1 @p!@p!қ`=/^f;+寞 ܻ=Ыot)h{]oG_O_d X/kReKFZx))oB4RS{3`Po,rϨ,Pu9xg̿y&Vkoà RYb-_]"U rR(5b%uth17ϭrq~G1%*XM>߭ 7GQrЎRkUwlkluG|<󨱢e\gbg>[3yM>C=~15{m BY%xTd9Gx'|9|i+ b)[zeʥ)ď#}hf fiѼ:'ZfZ]ejF[RP/#5.mӪuX9k{y۩gQͧĀxX}0)WO%@KhE2^=_1O!B!#P;'r3rJ~t x>/2P+t{VY=4- ֆs6C#nZg)n`AP BR{p4Iʧtb`ܿs[wޥ[:s>m yz~r8G;{`r˵r~O_S-e X'7[2_!>V.Xm%Wk/k{z?Mqۣ5f\Nؓo̓[9w`_s-)sꦮKʕSKM5Y`e0"gPj/O*Z-Mzޥ>UNk#1}#LdHg֓vkW8֫4k T]"{}oDo-/\_xR#hX=\zk9qOJh%ϸjIO ]a1?Jn%[#gH"䯑kzУT>gsrY^Mi!أ>pk;-dፉp3gmr2mZ0Aoko̒+3J-n12%$飌ty^}%{ޕ~FI#romX{Mcx]7uZ6i̽s敛TΒպI˺sQ,e-ƈ(g暍tRzb'! ?[ %S;DSSe3sUZ;>ɁB-.nrZQ+gm/o;>+M1vhv+[X{WW@/h{Ò eK۴ Iyۢ!-YE!!KaЂe'<)_o(eh}k- i; “O-)?My4r^Qwr?Q({P8Qg[^F pTYC ׬rBg!xpo|{P' fPos䠽mi'x*)Y,2VV8;9"`"@!o{B`9o#8@\1F ^hN7Y)ɢt!FLHb=ߺۼ>wo) T*{D|Jv0JH c7` 4Sȣx=Ck%w|l4kEJZ4BnӒEn<2(?DϑU= yTG {xNKg_sH^` &RDGkyBFɾ7ӷV#nyDHcchb%LmNe(Bf{w6z0P[5 ^b RT 7ےg7`#GG =iY *oѼ%ެjAv>-Au[{ w{܍\^03LK dw(z;w6ȏ&羚 >g Fkk0k͒ᐻ!()Hs/c%Mu_#R f{폒LX{6Bm/m dQv :m>bQmtpE7N.; ]#0@߃IK,n/ׂB>rR^trkK6zv5j~& xrെ@C@ȫ,ߕI>L<Iɓt8 AR7cYho];xr*uk[֠C3k~G*pm̂'6տ-VeXСLDHcdt/ ʹJ\(7skZNkJmt`!t$b HHA7D9z wT"?3KOC݈CV@V)Ey4 sJ#I{R24,?Cٿ0}(!:HQzWv6OmVcP2&z  :10f٠~PnH!<*.BOmn\>X~T\9?r0z:ЮƠl/L 0'ѡf寥cZ5{qZ^-iױOUHŰGJ'f;ePʧt! 2"V3R&WQcaK E7M^#b AAD V%HGhe jAʭ%徚C4MwC7τ%}KK/x *@ߵYΕ_r[bxn@T{iҷ7:8Ga$|*)ާmoOp$K>RR>QAC@mhgV75`ȩymjMuOP9Ϡ;=>}V4Ք!OT'ʫ%7wСzBg7 7s͘C'x9L^ R$#=A\]pHg^2j ʇ>|i4p 4p=x4p)=hE--XFe8ixHR/#R5tFo]ϥOs[.!G5ɐp= s1P3ZSKoD2 '$%_v@iSvoI-;ܯj=ր!>|KqkUg>Gnk7#%Ozzܟ :*ؒbPύ}!&[Jᾥ]Wi6JD"oWo*ij@ty_ۿ+gͬsu| JLR#Zz35%vr3o~)wYNki^o;Z Z%oUVAPa~V~s:9wqMx#<[*+%G=ss3Z?t\3뾇xPw:s ހsY(y}^Ζ|F= =6FxRk <57z:']Hf? \yR9#Ge_[&ZseFo›^#|k֙|V7``XCA0nvņ`d1(7!7`SC#`slh GC06F`IDAT x#;&׹ߧyz)Z M=ҷ.ь'x@r= i( ywt˸t8߯y[)rlN1~t>W({Z%r?͑QDVo=oNnKzfn"=/rj߭R,I~, u0){>/c`7sSG͒\jvmY`1^-M)ntub̲6vfǧ&> f%9׳f.5np}W̳v aT.ekP*k昺%ODm-c΍nu[=\krxjҲl6<\%n ^z>us^rQģV}*S1?Voq䣕ѻz :ֽ+Z뫹wS3 ""GD!m-{zz* VErkj[2t>.ނ\_ z3f814uW#+*)܆?0MLf/fw;rr{M$D2 +`/f3׷kW2{o_ ~^/uos_Bn jϑ{7fK)>£5U{1V.oq翟(ryf[e%#~,*Ů'2Ө=YU. J{ΏjFɭu ?Jr+mKХ~[Wi?Gqcăk̤ tJnl鷹MR5r[so|?ҿw r{Vl'k?ڌb2%wa75rCn໽06Cd~u_ Pڝ +]_toQ8~dH\t'mz /1ހCiBh/z^J_*'D)FYVR}[ ?g!` U荺S| ҡiY5A6oH[^` !:ubIzzVt3A78QKa<X#XTz'ϾN=|Ws,ץiмY;HyW+Km~ ){ -\JQ:a1%#4"DiL&0nAFJ8#++2O(S0W ۣF, E :_)|e[ۗ9w8WS|,riUF@D!~9TK9rhւyZv DvOMv(k!R߈N2ʿs}%9B@5,'ˠĩL,JG +`5pzճՅ!. hz O4ZAo[1f q|ik qeF^=^׿ xڷT,r2v5riJk+Z~yuZ6iĦ(ɴ^9e iM +dbXgKrSf fUZ±s5Tx`}'n|K{y۩gQgdM1 !0z`Xy"5A?^lvEhwf\IENDB`sugarjar-4.0.0/images/subfeature-smartlog.png000066400000000000000000000106241523615150300213030ustar00rootroot00000000000000PNG  IHDRmx jsRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME -IDATxQr( @q*7gb?v%$\84`4:B1+BvC 2ȇvzr}ֿG P<ڴ~ZGʽywT ?w./ߢk]zvOJrOWYE/yir}f-Ѿ~x\)h~kM[Лlg̺wPr^~8W(rؕo+s3֥h_r.R^T$Ok==:i)7M/_jk݊haxtk^-冀tזZ֟C@K?&՚ZuՔk]C%-$7kQρ2`_jݷ0:}±^\_xS#Vߢ$ٟ\ \]=cZnO=g#/=eόdhGutsнS|,ƛ< 08Gt^gl3fam+/u^:,g`K5yz `^^kDݺqkȐ,r2 ugύyZYh`怇yZ]Ӛu6}k"+[֜=ײJʿFnҲu6;ZmOG==ꙶynCŚ׬\zF!jmG&$ -yCmr #' ;Gx!Zۈ9^lj!υ! /u}~~[-kR>rki AmQנ9D⥥grW<,Q@2B֯)^KSJ+)7{z?鳹| Yu/Lz]\2ՖW]}Qpqqٮ)5O0j"}gm ;uZC*譬< D% ɍ3- ?)7 S\M}s=*ujxzPՂJi ڣ=g0ȝvf٤ *_[F4w4{+TƀFS#s%}OG([FBԧIo*RUhz$C@k(I^/ɧs9icc2 ,Җ ףT蔿WF@;j14!vS?]< J-M_ހʇsΙUvLƭf#;\Cp czPSO[zwJށ <=o)83Fvi]=&WYuT*᫗%԰9hW5[Ԃ\ԵW=/]`__C0^'ӀlCC84[@TK^}Y0Gt A`AA`p2E!R>me!4tz0,%)`G! C kСtF]][ 78ȥyA0<-3E-q]CԼ9!1myzi8éy JG9M} B%nI;` ;~x0 pO:Y\Kz͂-_ٗ6때԰ i0z7 M RZaUN4lf<!~tljGB,[沙/ Q ڛ}tw?7psdiSҳj|05H&MO0g{VRd(88h@E#_Kqq#ٕH3C#5k]=evys\|Ol R$)ؒbP/}_( [кOQʧ4CK[kaaό~Ҳ5Ht%`\CgC EGN5tr3g>RRȲ[*OΆ/)|B2(J8Ck#ZI̜;7#<{*+>w3]',g,bx Ee.3ͻh-3>7ï<>,w93 F-jܺ.6~& ހ8߈#U*k={m4He'x6H{׿Y rgu[HfCE.p,-R8_V#έ]y5Yk_!?asN{sRN˸@4GFIƅ zL9m9Y] 8_ `gm]Rw@NQ bs4fyE9J5K:ZjuLm+]L;f|KisLe7Q'7+ɹ5p5qVm,c|R_[J?g;[D\:V׺E]=cMnOMk, KBK(=u{^rQbfQgܧ-*;SP28A>ZyAZ]Ϛz)oϫ);?gԌvbe=^HA&JcN>YnIL(ɫF%C' |x ~*Bsg7|X}Ƹ<ո2mk5eVnGnu|l( ۿS^3ەwk}T2Ҳ7 ZFV/YP/!YjsލRKK3 ^|ޛ0|i: ͧ}RR=[HjhkPz(י=l`X3HGF])>K P R~^7둾jCj+<3 ;3X:ubIzzV>rJhݾ~*E$HﺗeO:i_vk}5sR{`,nm𜖓rhXyzjM5dm-)-k#RXUh`ݷ0:}b^\_xS#^otI{o-3/jQ^1h5]kBzO!~`.E3یA?zcm 4:MK'PZ6QՂ唝uu9,mv۞~e}z{ԳDKj UZ±3 5 .Pm ;&)7͝icp!Zۈq9^+}CmsBBc OIENDB`sugarjar-4.0.0/images/subfeature.png000066400000000000000000000042731523615150300174600ustar00rootroot00000000000000PNG  IHDR.ZqsRGB,gAMA a cHRMz&u0`:pQ< pHYs~tIME "|uIDATx]a* LQ*<6c0#UۯKcc ş8~AA[KA  v~߿!cwow7?!99g~|ԃ\6uJH'h?/[we=wɞn?leq$tr۔^y ~iW\ed0Bn9޶}moE3S&妋S-}ӇSn)QX0iVuyx)8Ψi77M=Vz5ґw0,%KՖ{xNԵTvr|Jrɢ&7+?* W&5{(cVL׶ZiS7?oxV%>v5ѯ{V=@ ;Aim[&E^ eXe_ t"R{l1Rgk+Z>9v.qֳ9Z>=VQt#VQr+94{䥁HNlME䀦#Mq(ŸW_D#7Z!~#2 *ͯ&z%$!7也π ܯ(^jn*:o}Vd&Ȑ,rGFsD+ KiT!-^wJt(j->[#Lu^XV3ZQ<=gU"gydjj~B^i843VU_^=E_…ЗN+#@Wzt&h}rk*18}} @Aĺ`A ` ofA0ϣxn$k2_{ն_A(ңXÝB 4'X\ :_U!kK -LA+Ӱc:})>?+o @~^~EO\J,_̓wnHtS>%~#l}+?Z^7+N;~K_~ vUQv.ʣ]L|xoZV-bo rڟ蜎 A*7~G L\OE'f*"9]U9/{Ҁ)& ׾% U:F mb (Õ*+k&<-_֜ [L@(}޲**ЏjW=_ -kgt,e0Y_;^l\/_h'^1LDgz'廖|_@P0vIP ij 0 i~wo=bwF:ҭah莪=[M~Mϥ(fwֱ,;V@I;Ɂ` N*Γ7hߜRhw;oZtX NuF >ޱJCefȬvSMоH2LoX$2Rj4\ZӻuOEN 2[ZZD}9[Sq b@ bln=uFﳇl +q {iq -tˣ&O. =tN'Pu.4Ae=*()ʵ)n|^ xSӚ}ENF_)Ry-jgrGKvbWqQ*WKNd f֜%)P *rCDuh"'wD#'כ~ @X:  b]|) ` @A  AO+ IENDB`sugarjar-4.0.0/lib/000077500000000000000000000000001523615150300141005ustar00rootroot00000000000000sugarjar-4.0.0/lib/sugarjar/000077500000000000000000000000001523615150300157165ustar00rootroot00000000000000sugarjar-4.0.0/lib/sugarjar/commands.rb000066400000000000000000000310531523615150300200460ustar00rootroot00000000000000require 'mixlib/shellout' require_relative 'commands/amend' require_relative 'commands/bclean' require_relative 'commands/branch' require_relative 'commands/checks' require_relative 'commands/debuginfo' require_relative 'commands/feature' require_relative 'commands/modernize_config' require_relative 'commands/pullsuggestions' require_relative 'commands/push' require_relative 'commands/smartclone' require_relative 'commands/smartpullrequest' require_relative 'commands/up' require_relative 'forge' require_relative 'git' require_relative 'log' require_relative 'repoconfig' require_relative 'version' class SugarJar # This is the workhorse of SugarJar. Short of #initialize, all other public # methods are "commands". Anything in private is internal implementation # details. class Commands MAIN_BRANCHES = %w{master main}.freeze def initialize(options) SugarJar::Log.debug("Commands.initialize options: #{options}") @config = options @repo_config = SugarJar::RepoConfig.config SugarJar::Log.debug("Repoconfig: #{@repo_config}") @checks = {} @main_branch = nil @main_remote_branches = {} @host_config = nil if SugarJar::Git.in_repo? @host_config = gen_host_config if @host_config['forge_type'] # initialize instance var here so checks run... forge end end return if options['no_change'] set_commit_template if @repo_config['commit_template'] end private def forge return @forge if @forge unless @host_config['forge_type'] raise 'Attempted to create Forge object before forge-type known!' end @forge = SugarJar::Forge.new( @host_config['forge_type'], @host_config['forge_host'] ) end def gen_host_config(host = nil) host ||= _determine_forge_host _config_for_host(host) || {} end def _config_for_host(host) SugarJar::Log.debug("config_for_host called for #{host}") r = @config.dig('host_configs', 'default').dup || {} r.merge!(@config.dig('host_configs', host) || {}) r['forge_host'] = host # if people specified a forge_type, honor it r['forge_type'] ||= _determine_forge_type(host) # there are a few things we allow the CLI to override r.merge!(@config['_cli_overrides'] || {}) SugarJar::Log.debug("Determined config for host: #{r}") r end def forked_repo(repo, username) host = @host_config&.dig('forge_host') || extract_host(repo) || @config['default_forge_host'] raise 'Cannot determine host' unless host repo = extract_repo(repo) "git@#{host}:#{username}/#{repo}.git" end # gh utils will default to https, but we should always default to SSH # unless otherwise specified since https will cause prompting. def canonicalize_repo(repo) # if they fully-qualified it, we're good return repo if repo.start_with?('http', 'git@', 'ssh://') host = @host_config&.dig('forge_host') || extract_host(repo) || @config['default_forge_host'] # otherwise, it's a shortname cr = "git@#{host}:#{repo}.git" SugarJar::Log.debug("canonicalized #{repo} to #{cr}") cr end def _determine_forge_host # DO NOT USE in the general case - ONLY for filling in @host_config # (doesn't consult host_config). Use @host_config['forge_host'] # in general. # if we're in a repo, use the hostname of the remote if SugarJar::Git.in_repo? url = remote_url_map.values.first return extract_host(url) end @config['default_forge_host'] end def repo_shortname(repo) # if it's already a shortname, return return repo unless repo.start_with?('http', 'git@') repo_name = extract_repo(repo) org_name = extract_org(repo) [org_name, repo_name].join('/') end def set_commit_template unless SugarJar::Git.in_repo? SugarJar::Log.debug('Skipping set_commit_template: not in repo') return end realpath = if @repo_config['commit_template'].start_with?('/') @repo_config['commit_template'] else File.join( SugarJar::Git.repo_root, @repo_config['commit_template'], ) end unless File.exist?(realpath) die( "Repo config specifies #{@repo_config['commit_template']} as the " + 'commit template, but that file does not exist.', ) end s = git_nofail('config', '--local', 'commit.template') unless s.error? current = s.stdout.strip if current == @repo_config['commit_template'] SugarJar::Log.debug('Commit template already set correctly') return else SugarJar::Log.warn( "Updating repo-specific commit template from #{current} " + "to #{@repo_config['commit_template']}", ) end end SugarJar::Log.debug( 'Setting repo-specific commit template to ' + "#{@repo_config['commit_template']} per sugarjar repo config.", ) git( 'config', '--local', 'commit.template', @repo_config['commit_template'] ) end def assert_in_repo! return if SugarJar::Git.in_repo? die('sugarjar must be run from inside a git repo') end def dirty_check! return unless dirty? if @config['ignore_dirty'] SugarJar::Log.warn( 'Your repo is dirty, but --ignore-dirty was specified, so ' + 'carrying on anyway.', ) else SugarJar::Log.error( 'Your repo is dirty, so I am refusing to continue. Please commit ' + 'or amend first (or use --ignore-dirty to override).', ) exit(1) end end def determine_main_branch(branches) if branches.include?('main') 'main' elsif branches.include?('master') 'master' end end def main_branch @main_branch = determine_main_branch(all_local_branches) end def main_remote_branch(remote) @main_remote_branches[remote] ||= determine_main_branch(all_remote_branches(remote)) end def checkout_main_branch git('checkout', main_branch) end def all_remote_branches(remote = 'origin') branches = [] git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line| next unless line.start_with?("refs/remotes/#{remote}/") branches << branch_from_ref(line.strip, :remote) end branches end def all_local_branches git( 'branch', '--format', '%(refname)' ).stdout.lines.map do |line| if line.start_with?('(') SugarJar::Log.debug("Skipping meta-branch: #{line.strip}") next end branch_from_ref(line.strip) end end def all_remotes git('remote').stdout.lines.map(&:strip) end def remote_url_map m = {} git('remote', '-v').stdout.each_line do |line| name, url, = line.split m[name] = url end m end def current_branch branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip) end def fetch_upstream us = upstream fetch(us) if us end def fetch(remote) git('fetch', remote) end # determine if this branch is based on another local branch (i.e. is a # subfeature). Used to figure out of we should stack the PR def subfeature?(base) all_local_branches.reject { |x| x == most_main }.include?(base) end def tracked_branch(branch = nil, fallback: true) curr = current_branch git('checkout', branch) if branch && branch != curr s = git_nofail( 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}' ) git('checkout', curr) if branch && branch != curr if s.error? branch = fallback ? most_main : nil SugarJar::Log.debug("No specific tracked branch, using #{branch}") else branch = s.stdout.strip SugarJar::Log.debug( "Using explicit tracked branch: #{branch}, use " + '`git branch -u` to change', ) end branch end def most_main us = upstream if us "#{us}/#{main_branch}" else main_branch end end def upstream return @remote if @remote remotes = all_remotes SugarJar::Log.debug("remotes is #{remotes}") if remotes.empty? @remote = nil elsif remotes.length == 1 @remote = remotes[0] elsif remotes.include?('upstream') @remote = 'upstream' elsif remotes.include?('origin') @remote = 'origin' else raise 'Could not determine "upstream" remote to use...' end @remote end def upstream_org us = upstream remotes = remote_url_map extract_org(remotes[us]) end # Whatever org we push to, regardless of if this is a fork or not def push_org url = remote_url_map['origin'] extract_org(url) end def repo_name # Take the url of the most-upstream remote, then pull the repo # name from that extract_repo(remote_url_map[upstream]) end def color(string, *colors) if @config['color'] pastel.decorate(string, *colors) else string end end def pastel @pastel ||= begin require 'pastel' Pastel.new end end def fprefix(name) return name unless @host_config['feature_prefix'] return name if name.start_with?(@host_config['feature_prefix']) return name if all_local_branches.include?(name) newname = "#{@host_config['feature_prefix']}#{name}" SugarJar::Log.debug( "Munging feature name: #{name} -> #{newname} due to feature prefix", ) newname end def dirty? s = git_nofail('diff', '--quiet') s.error? end def repo_dir_name SugarJar::Git.repo_root.split('/').last end def extract_org(repo) if repo.start_with?('http') repo.split('://').last.split('/')[1..-2].join('/') elsif repo.start_with?('git@') repo.split(':').last.split('/')[0..-2].join('/') else # assume they passed in a cli-friendly shortname repo.split('/')[0..-2].join('/') end end def extract_repo(repo) File.basename(repo, '.git') end def extract_host(repo) if repo.start_with?('git@') repo.split(':').first.split('@').last elsif repo.start_with?('ssh://') repo.split('/')[2].split('@').last elsif repo.start_with?('http') repo.split('/')[2] end end def die(msg) SugarJar::Log.fatal(msg) exit(1) end def release_branches @repo_config['release_branches'] || [] end def worktree_branches worktrees.values.map do |wt| branch_from_ref(wt['branch']) end end def worktrees root = SugarJar::Git.repo_root s = git('worktree', 'list', '--porcelain') s.error! worktrees = {} # each entry is separated by a double newline s.stdout.split("\n\n").each do |entry| # then each key/val is split by a new line with the key and # the value themselves split by a whitespace tree = entry.split("\n").to_h(&:split) # Skip the one next if tree['worktree'] == root worktrees[tree['worktree']] = tree end worktrees end def branch_from_ref(ref, type = :local) # local branches are refs/head/XXXX # remote branches are refs/remotes//XXXX base = type == :local ? 2 : 3 ref.split('/')[base..].join('/') end def remote_from_ref(ref) return nil unless ref.start_with?('refs/remotes/') ref.split('/')[2] end def git(*) SugarJar::Git.run(*, :color => @config['color']) end def git_nofail(*) SugarJar::Git.run_nofail(*, :color => @config['color']) end def _determine_forge_type(host = nil) if host case host when /gitlab/ return 'gitlab' when /github/ return 'github' when /forgejo|codeberg/ return 'forgejo' end end if SugarJar::Git.in_repo? gl = remote_url_map.values.any? { |x| x.include?('gitlab') } return gl ? 'gitlab' : 'github' end # in smartclone mode, when creating the object, we will in fact # be unable to determine this, that's expacted SugarJar::Log.debug('Unable to determine forge_type!') end end end sugarjar-4.0.0/lib/sugarjar/commands/000077500000000000000000000000001523615150300175175ustar00rootroot00000000000000sugarjar-4.0.0/lib/sugarjar/commands/amend.rb000066400000000000000000000006361523615150300211350ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def amend(*) assert_in_repo! # This cannot use shellout since we need a full terminal for the editor exit(system(SugarJar::Util.which('git'), 'commit', '--amend', *)) end def qamend(*) assert_in_repo! SugarJar::Log.info(git('commit', '--amend', '--no-edit', *).stdout) end alias amendq qamend end end sugarjar-4.0.0/lib/sugarjar/commands/bclean.rb000066400000000000000000000163531523615150300213000ustar00rootroot00000000000000class SugarJar class Commands def lbclean(name = nil) assert_in_repo! name ||= current_branch name = fprefix(name) should_skip, why = skip_branch_info(name) if should_skip msg = "#{name}: #{color('skipped', :yellow)}" msg << " (#{why})" if why SugarJar::Log.warn(msg) return end if clean_branch(name) SugarJar::Log.info("#{name}: #{color('reaped', :green)}") else die( "#{color("Cannot clean #{name}", :red)}! there are unmerged " + "commits; use 'git branch -D #{name}' to forcefully delete it.", ) end end alias localbranchclean lbclean # backcompat alias bclean lbclean def rbclean(name = nil, remote = nil) assert_in_repo! name ||= current_branch name = fprefix(name) remote ||= 'origin' ref = "refs/remotes/#{remote}/#{name}" if git_nofail('show-ref', '--quiet', ref).error? SugarJar::Log.warn("Remote branch #{name} on #{remote} does not exist.") return end if clean_branch(ref, :remote) SugarJar::Log.info("#{ref}: #{color('reaped', :green)}") else die( "#{color("Cannot clean #{ref}", :red)}! there are unmerged " + "commits; use 'git push #{remote} -d #{name}' to forcefully delete " + ' it.', ) end end alias remotebranchclean rbclean def gbclean(name = nil, remote = nil) assert_in_repo! name ||= current_branch remote ||= 'origin' lbclean(name) rbclean(name, remote) end alias globalbranchclean gbclean def lbcleanall assert_in_repo! curr = current_branch worktree_branches all_local_branches.each do |branch| # skip_branch info will check for MAIN_BRANCHES, but we # quietly skip them. next if MAIN_BRANCHES.include?(branch) should_skip, why = skip_branch_info(branch) if should_skip msg = "#{branch}: #{color('skipped', :yellow)}" msg << " (#{why})" if why SugarJar::Log.info(msg) next end if clean_branch(branch) SugarJar::Log.info("#{branch}: #{color('reaped', :green)}") else SugarJar::Log.info("#{branch}: skipped") SugarJar::Log.debug( "There are unmerged commits; use 'git branch -D #{branch}' to " + 'forcefully delete it)', ) end end # Return to the branch we were on, or main if all_local_branches.include?(curr) git('checkout', curr) else checkout_main_branch end end alias localbranchcleanall lbcleanall # backcomat alias bcleanall lbcleanall def rbcleanall(remote = nil) assert_in_repo! curr = current_branch remote ||= 'origin' all_remote_branches(remote).each do |branch| if (MAIN_BRANCHES + ['HEAD']).include?(branch) SugarJar::Log.debug("Skipping #{branch}") next end ref = "refs/remotes/#{remote}/#{branch}" if clean_branch(ref, :remote) SugarJar::Log.info("#{ref}: #{color('reaped', :green)}") else SugarJar::Log.info("#{ref}: skipped") SugarJar::Log.debug( "There are unmerged commits; use 'git branch -D #{branch}' to " + 'forcefully delete it)', ) end end # Return to the branch we were on, or main if all_local_branches.include?(curr) git('checkout', curr) else checkout_main_branch end end alias remotebranchcleanall rbcleanall def gbcleanall(remote = nil) assert_in_repo! bcleanall rcleanall(remote) end alias globalbranchcleanall gbcleanall private # rubocop:disable Naming/PredicateMethod def clean_branch(name, type = :local) undeleteable = MAIN_BRANCHES.dup undeleteable << 'HEAD' if type == :remote die("Cannot remove #{name} branch") if undeleteable.include?(name) SugarJar::Log.debug('Fetch relevant remote...') fetch_upstream fetch(remote_from_ref(name)) if type == :remote return false unless safe_to_clean?(name) SugarJar::Log.debug('branch deemed safe to delete...') if type == :remote remote = remote_from_ref(name) branch = branch_from_ref(name, :remote) git('push', remote, '--delete', branch) else checkout_main_branch git('branch', '-D', name) rebase end true end # rubocop:enable Naming/PredicateMethod def safe_to_clean?(branch) # cherry -v will output 1 line per commit on the target branch # prefixed by a - or + - anything with a - can be dropped, anything # else cannot. SugarJar::Log.debug("Checking if branch #{branch} is safe to delete...") if branch.start_with?('refs/remotes/') remote = remote_from_ref(branch) tracked = main_remote_branch(remote) else tracked = tracked_branch(branch) end out = git( 'cherry', '-v', tracked, branch ).stdout.lines.reject do |line| line.start_with?('-') end if out.empty? SugarJar::Log.debug( "cherry-pick shows branch #{branch} obviously safe to delete", ) return true end # if the "easy" check didn't work, it's probably because there # was a squash-merge. To check for that we make our own squash # merge to upstream/main and see if that has any delta # First we need a temp branch to work on tmpbranch = "_sugar_jar.#{Process.pid}" git('checkout', '-b', tmpbranch, tracked) s = git_nofail('merge', '--squash', branch) if s.error? cleanup_tmp_branch(tmpbranch, branch, tracked) SugarJar::Log.debug( 'Failed to merge changes into current main. This means we could ' + 'not figure out if this is merged or not. Check manually and use ' + "'git branch -D #{branch}' if it is safe to do so.", ) return false end s = git('diff', '--staged') out = s.stdout SugarJar::Log.debug("Squash-merged diff: #{out}") cleanup_tmp_branch(tmpbranch, branch, tracked) if out.empty? SugarJar::Log.debug( 'After squash-merging, this branch appears safe to delete', ) true else SugarJar::Log.debug( 'After squash-merging, this branch is NOT fully merged to main', ) false end end def cleanup_tmp_branch(tmp, backto, tracked = nil) tracked ||= tracked_branch # Reset any changes on our temp branch from various merge attempts # so we're in a state we know we can 'checkout' away from. git('reset', '--hard', tracked) # checkout whatever branch we were on before git('checkout', backto) # delete our temp branch git('branch', '-D', tmp) end def skip_branch_info(name) return true, 'main branch' if MAIN_BRANCHES.include?(name) wt_branches = worktree_branches rel_branches = release_branches return true, 'worktree' if wt_branches.include?(name) return true, 'release branch' if rel_branches.include?(name) [false, nil] end end end sugarjar-4.0.0/lib/sugarjar/commands/branch.rb000066400000000000000000000021571523615150300213060ustar00rootroot00000000000000class SugarJar class Commands def checkout(*args) assert_in_repo! # Pop the last arguement, which is _probably_ a branch name # and then add any featureprefix, and if _that_ is a branch # name, replace the last arguement with that name = args.last bname = fprefix(name) if all_local_branches.include?(bname) SugarJar::Log.debug("Featurepefixing #{name} -> #{bname}") args[-1] = bname end s = git('checkout', *args) SugarJar::Log.info(s.stderr + s.stdout.chomp) end alias co checkout def br assert_in_repo! SugarJar::Log.info(git('branch', '-v').stdout.chomp) end def binfo assert_in_repo! SugarJar::Log.info(git( 'log', '--graph', '--oneline', '--decorate', '--boundary', "#{tracked_branch}.." ).stdout.chomp) end # binfo for all branches def smartlog assert_in_repo! SugarJar::Log.info(git( 'log', '--graph', '--oneline', '--decorate', '--boundary', '--branches', "#{most_main}.." ).stdout.chomp) end alias sl smartlog end end sugarjar-4.0.0/lib/sugarjar/commands/checks.rb000066400000000000000000000131641523615150300213110ustar00rootroot00000000000000require_relative '../git' class SugarJar class Commands def lint assert_in_repo! # does not use dirty_check! as we want a custom message if dirty? if @config['ignore_dirty'] SugarJar::Log.warn( 'Your repo is dirty, but --ignore-dirty was specified, so ' + 'carrying on anyway. If the linter autocorrects, the displayed ' + 'diff will be misleading', ) else SugarJar::Log.error( 'Your repo is dirty, but --ignore-dirty was not specified. ' + 'Refusing to run lint. This is to ensure that if the linter ' + 'autocorrects, we can show the correct diff.', ) exit(1) end end exit(1) unless run_check('lint', false) end def unit assert_in_repo! exit(1) unless run_check('unit', false) end def get_checks_from_command(type) return nil unless @repo_config["#{type}_list_cmd"] cmd = @repo_config["#{type}_list_cmd"] short = cmd.split.first unless File.exist?(short) SugarJar::Log.error( "Configured #{type}_list_cmd #{short} does not exist!", ) return false end s = Mixlib::ShellOut.new(cmd).run_command if s.error? SugarJar::Log.error( "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}", ) return false end output = s.stdout if output.start_with?("---\n") begin checks = YAML.safe_load(s.stdout) return checks rescue Psych::SyntaxError => e SugarJar::Log.error( "#{type}_list_cmd (#{cmd}) produced invalid YAML: #{e.message}", ) return false end end # otherwise it's one line per command output.split("\n") end # determine if we're using the _list_cmd and if so run it to get the # checks, or just use the directly-defined check, and cache it def get_checks(type) return @checks[type] if @checks[type] ret = get_checks_from_command(type) if ret SugarJar::Log.debug("Found #{type}s: #{ret}") @checks[type] = ret # if it's explicitly false, we failed to run the command elsif ret == false @checks[type] = false # otherwise, we move on (basically: it's nil, there was no _list_cmd) else SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}") @checks[type] = @repo_config[type] || [] end @checks[type] end # autorun is true when we're running from push, and false when someone # ran 'lint' or 'unit' directly # # In the case of a autorun, if a linter changes the code, we require # either the user amend, or bail out. If it's a manual run, then we # allow them to just go on. def run_check(type, autorun) repo_root = SugarJar::Git.repo_root Dir.chdir repo_root do checks = get_checks(type) # if we failed to determine the checks, then the checks have effectively # failed return false unless checks checks.each do |check| if check.is_a?(Hash) command = check['command'] name = check['name'] || command.split.first else command = check name = check.split.first end SugarJar::Log.debug("Running #{type} #{name}: #{command}") skip_redo = false short = command.split.first if short.include?('/') short = File.join(repo_root, short) unless short.start_with?('/') unless File.exist?(short) SugarJar::Log.error("Configured #{type} #{short} does not exist!") end elsif !SugarJar::Util.which_nofail(short) SugarJar::Log.error("Configured #{type} #{short} does not exist!") return false end s = Mixlib::ShellOut.new(command).run_command # Linters auto-correct, lets handle that gracefully if type == 'lint' && dirty? SugarJar::Log.info( "[#{type}] #{name}: #{color('Corrected', :yellow)}", ) SugarJar::Log.warn( "The linter modified the repo. Here's the diff:\n", ) puts git('diff').stdout loop do options = [ '[q]uit and inspect', '[a]mend the changes to the current commit and re-run', ] options << '[i]gnore the changes and keep going' unless autorun msg = "\nWould you like to\n\t" + options.join("\n\t") + "\n > " $stdout.print(msg) ans = $stdin.gets.strip case ans when /^q/ SugarJar::Log.info('Exiting at user request.') exit(1) when /^a/ qamend('-a') # break here, if we get out of this loop we 'redo', assuming # the user chose this option break when /^i/ unless autorun skip_redo = true break end end end redo unless skip_redo end if s.error? SugarJar::Log.info( "[#{type}] #{name} #{color('failed', :red)}, output follows " + "(see debug for more)\n#{s.stdout}", ) SugarJar::Log.debug(s.format_for_exception) return false end SugarJar::Log.info( "[#{type}] #{name}: #{color('OK', :green)}", ) end end end end end sugarjar-4.0.0/lib/sugarjar/commands/debuginfo.rb000066400000000000000000000006061523615150300220100ustar00rootroot00000000000000require 'json' class SugarJar class Commands def debuginfo(*args) puts "sugarjar version #{SugarJar::VERSION}" forge.all_versions.each do |v| puts v end puts git('version').stdout puts "Config: #{JSON.pretty_generate(args[0])}" return unless @repo_config puts "Repo config: #{JSON.pretty_generate(@repo_config)}" end end end sugarjar-4.0.0/lib/sugarjar/commands/feature.rb000066400000000000000000000036701523615150300215050ustar00rootroot00000000000000class SugarJar class Commands def feature(name, base = nil) assert_in_repo! SugarJar::Log.debug("Feature: #{name}, #{base}") name = fprefix(name) die("#{name} already exists!") if all_local_branches.include?(name) rel_branches = release_branches if base # If the user specified a base branch (sf mything base) # we check if is a release branch and if so, we make # this track / if rel_branches.include?(base) newbase = "#{upstream}/#{base}" SugarJar::Log.info( "Base branch #{base} is a release branch, setting it to track " + newbase, ) base = newbase else fbase = fprefix(base) base = fbase if all_local_branches.include?(fbase) end elsif rel_branches.include?(name) # If the user did NOT specify a base *and* this new feature is # a release branch, check it out tracking the upstream release # branch instead of main base = "#{upstream}/#{name}" SugarJar::Log.info( "Feature #{name} is a release branch, setting it to track #{base}", ) else # otherwise, fallback to most-main base ||= most_main end # If our base is a local branch, don't try to parse it for a remote name unless all_local_branches.include?(base) base_pieces = base.split('/') git('fetch', base_pieces[0]) if base_pieces.length > 1 end git('checkout', '-b', name, base) git('branch', '-u', base) SugarJar::Log.info( "Created feature branch #{color(name, :green)} based on " + color(base, :green), ) end alias f feature # alias for "feature ' def subfeature(name) assert_in_repo! SugarJar::Log.debug("Subfature: #{name}") feature(name, current_branch) end alias sf subfeature end end sugarjar-4.0.0/lib/sugarjar/commands/modernize_config.rb000066400000000000000000000074731523615150300234000ustar00rootroot00000000000000class SugarJar class Commands def modernizeconfig(config) require 'tempfile' require 'diffy' data = YAML.safe_load_file(config) data = _modernize(data).to_yaml tfile = Tempfile.new('sugarjar') tfile.write(data) tfile.close diff = Diffy::Diff.new( config, tfile.path, :source => 'files', :context => 2, :include_diff_info => true, :format => @config['color'] ? 'color' : 'text', ).to_s puts "Here is the diff from your existing config to the new one:\n" puts diff options = [ '[u]pdate my config, saving my old one to .bak', '[w]rite new config to a temporary file for me to inspect', ] msg = "\nWould you like to\n\t" + options.join("\n\t") + "\n > " loop do $stdout.print(msg) ans = $stdin.gets.strip case ans when /^u/ old = "#{config}.bak" File.rename(config, old) File.write(config, data) puts "#{config} updated. Old config in #{old}" break when /^w/ new = "#{config}.new" File.write(new, data) puts "New config written to #{new}" break end end tfile.unlink end alias fixconfig modernizeconfig def _modernize(data) data['host_configs'] ||= {} data['host_configs']['default'] ||= {} # First, take anything that used to be a top-level config # that's now a host config and move it to host_configs->default %w{use_forks feature_prefix}.each do |tlc| # this needs to return true even if the value is false-y next unless data.key?(tlc) if data['host_configs']['default'][tlc] if data['host_configs']['default'][tlc] != data[tlc] SugarJar::Log.warn( "host_configs->default->#{tlc} already set to something" + " different than deprecated top-level #{tlc}, so just deleting" + ' old top-level config.', ) end else data['host_configs']['default'][tlc] = data[tlc] end # we delete it either way, just only warn if it's different data.delete(tlc) end # Users move into the default config, plus other userless hosts # of that type %w{github gitlab}.each do |forge| tlc = "#{forge}_user" site = "#{forge}.com" next unless data[tlc] # start with the "default site" for this user (github.com/gitlab.com) if data.dig('host_configs', site, 'user') if data['host_configs'][site]['user'] != data[tlc] SugarJar::Log.warn( "host_configs->#{site}->user already set to something" + " different than deprecated top-level #{tlc}, so just" + ' deleting old top-level config.', ) end else data['host_configs'][site] ||= {} data['host_configs'][site]['user'] = data[tlc] end # now loop through any existing hosts that have been configured that # are NOT those default hosts... IFthey don't have a user, populate # with the relevant top-level user as in the old world, we would have # fallen back to ${type}_user data['host_configs'].each do |host, cfg| next if host == site next unless host.include?(forge) next if cfg && cfg['user'] data['host_configs'][host] ||= {} data['host_configs'][host]['user'] = data[tlc] end # and finally, delete the top-level config data.delete(tlc) end # Remove any unused/deprecated configs. # # we don't use TOP_LEVEL_CONFIGS here as that includes things # that are cli-only data.slice(*SugarJar::Config::DEFAULTS.keys) end end end sugarjar-4.0.0/lib/sugarjar/commands/pullsuggestions.rb000066400000000000000000000014711523615150300233160ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def pullsuggestions assert_in_repo! dirty_check! src = "origin/#{current_branch}" fetch('origin') diff = git('diff', "..#{src}").stdout return unless diff && !diff.empty? puts "Will merge the following suggestions:\n\n#{diff}" loop do $stdout.print("\nAre you sure? [y/n] ") ans = $stdin.gets.strip case ans when /^[Yy]$/ git = SugarJar::Util.which('git') system(git, 'merge', '--ff', "origin/#{current_branch}") break when /^[Nn]$/, /^[Qq](uit)?/ puts 'Not merging at user request...' break else puts "Didn't understand '#{ans}'." end end end alias ps pullsuggestions end end sugarjar-4.0.0/lib/sugarjar/commands/push.rb000066400000000000000000000025441523615150300210300ustar00rootroot00000000000000class SugarJar class Commands def smartpush(remote = nil, branch = nil) assert_in_repo! _smartpush(remote, branch, false) end alias spush smartpush def forcepush(remote = nil, branch = nil) assert_in_repo! _smartpush(remote, branch, true) end alias fpush forcepush private def _smartpush(remote, branch, force) unless remote && branch remote ||= 'origin' branch ||= current_branch end dirty_check! unless run_prepush if @config['ignore_prerun_failure'] SugarJar::Log.warn( 'Pre-push checks failed, but --ignore-prerun-failure was ' + 'specified, so carrying on anyway', ) else SugarJar::Log.error('Pre-push checks failed. Not pushing.') exit(1) end end args = ['push', remote, branch] args << '--force-with-lease' if force puts git(*args).stderr end # rubocop:disable Naming/PredicateMethod def run_prepush @repo_config['on_push']&.each do |item| SugarJar::Log.debug("Running on_push check type #{item}") unless run_check(item, true) SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.") return false end end true end # rubocop:enable Naming/PredicateMethod end end sugarjar-4.0.0/lib/sugarjar/commands/smartclone.rb000066400000000000000000000074441523615150300222240ustar00rootroot00000000000000class SugarJar class Commands def smartclone(repo, *args) org = extract_org(repo) reponame = extract_repo(repo) host = extract_host(repo) || @config['default_forge_host'] unless host raise 'Failed to determine host. Please specify --default-forge-host,' + ' and file a bug.' end @host_config = gen_host_config(host) raise 'Failed to determine forge_type' unless @host_config['forge_type'] # force object recreation - the initial creation would not # have had the right data. Plus, we want it to check for the # right cli @forge = nil forge # set env vars of the host, just for completeness # since it's short-lived we just set both vars ENV['GH_HOST'] = host ENV['GL_HOST'] = host dir = if args.length.positive? && !args.first.start_with?('-') args.shift else reponame end forkname = @config['fork_name'] || reponame SugarJar::Log.info("Cloning #{reponame}...") # GH's 'fork' command (with the --clone arg) will fork, if necessary, # then clone, and then setup the remotes with the appropriate names. So # we just let it do all the work for us and return. # # Unless the repo is in our own org and cannot be forked, then it # will fail. if @host_config['use_forks'] && @host_config['user'] != org case forge.type when 'gitlab', 'forgejo' _separate_fork_clone(org, repo, dir, forkname, *args) else forge.run( 'repo', 'fork', '--clone', canonicalize_repo(repo), dir, '--fork-name', forkname, *args ) end # make the main branch track upstream Dir.chdir dir do git('branch', '-u', "upstream/#{main_branch}") end else git('clone', canonicalize_repo(repo), dir, *args) end SugarJar::Log.info('Remotes "origin" and "upstream" configured.') end alias sclone smartclone def _separate_fork_clone(_org, repo, dir, forkname, *) # The gitlab CLI is much less forgiving about already-forked # repos, and it has no option to clone to a differently-named # directory. So we have to special case it. extract_repo(repo) # glab requires a short-name for the fork command... shortname = repo_shortname(repo) # For both gitlab and forgejo we fork without clone: # * gitlab - it cannot clone to a chosen directory # * forgjo - it doesn't have an option to fork and clone in one # # Also note that for gitlab we must be explicit, if we don't # set --clone=false it will prompt. repo_ref = forge.type == 'gitlab' ? shortname : repo args = ['repo', 'fork', repo_ref, '--name', forkname] args << '--clone=false' if forge.type == 'gitlab' s = forge.run_nofail(*args) # It fails with: # 409 {message: [Project namespace name has already been taken, # Name has already been taken, Path has already been taken]} # # when there's already a fork... or if you happen to have a name # collision. There's no way to tell, so we assume it means we've # already forked. if s.error? if %w{gitlab forgejo}.include?(forge.type) && s.stderr.include?(' 409 ') SugarJar::Log.debug('Forking failed, probably already forked') else s.error! end end # Now we clone ourselves... git('clone', canonicalize_repo(repo), dir, *) Dir.chdir dir do # and then configure remotes properly git('remote', 'rename', 'origin', 'upstream') fork_url = forked_repo(repo, @host_config['user']) git('remote', 'add', 'origin', fork_url) end end end end sugarjar-4.0.0/lib/sugarjar/commands/smartpullrequest.rb000066400000000000000000000113521523615150300235020ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def smartpullrequest(*args) assert_in_repo! assert_common_main_branch! # does not use `dirty_check!` because we don't allow overriding here if dirty? SugarJar::Log.warn( 'Your repo is dirty, so I am not going to create a pull request. ' + 'You should commit or amend and push it to your remote first.', ) exit(1) end user_specified_base = args.include?('-B') || args.include?('--base') curr = current_branch base = tracked_branch if @config['pr_autofill'] SugarJar::Log.info('Autofilling in PR from commit message') num_commits = git( 'rev-list', '--count', curr, "^#{base}" ).stdout.strip.to_i if num_commits > 1 && forge.type == 'github' args.unshift('--fill-first') elsif forge.type == 'forgejo' args += _forgejo_pr_opts else args.unshift('--fill') end end base_opt = if forge.type == 'gitlab' '--target-branch' else '--base' end unless user_specified_base if subfeature?(base) if upstream_org != push_org SugarJar::Log.warn( 'Unfortunately you cannot based one PR on another PR when' + " using fork-based PRs. We will base this on #{most_main}." + ' This just means the PR "Changes" tab will show changes for' + ' the full stack until those other PRs are merged and this PR' + ' PR is rebased.', ) # nil is prompt, true is always, false is never elsif @config['pr_autostack'].nil? $stdout.print( 'It looks like this is a subfeature, would you like to base ' + "this PR on #{base}? [y/n] ", ) ans = $stdin.gets.strip args.unshift(base_opt, base) if %w{Y y}.include?(ans) elsif @config['pr_autostack'] args.unshift(base_opt, base) end elsif base.include?('/') && base != most_main # If our base is a remote branch, then use that as the # base branch of the PR args.unshift(base_opt, base.split('/').last) end end # : is the GH API syntax for: # look for a branch of name , from a fork in owner if forge.type == 'gitlab' # On GitLab, the head is the repo (org/repo) to use as the head # _repo_, and then branch is configured seperately (with -s), but # we don't need that since it defaults to the local branch name. # # Then we need --yes for it to not prompt us args.unshift('--head', "#{push_org}/#{repo_name}", '--yes') else # On GitHub/Forgeji, the head is the org and the *BRANCH* name to use as # the head branch... args.unshift('--head', "#{push_org}:#{curr}") end forge.run_with_system(_pr_cmd, 'create', *args) end alias spr smartpullrequest alias smartpr smartpullrequest private def _pr_cmd forge.type == 'gitlab' ? 'mr' : 'pr' end def assert_common_main_branch! upstream_branch = main_remote_branch(upstream) unless main_branch == upstream_branch die( "The local main branch is '#{main_branch}', but the main branch " + "of the #{upstream} remote is '#{upstream_branch}'. You probably " + "want to rename your local branch by doing:\n\t" + "git branch -m #{main_branch} #{upstream_branch}\n\t" + "git fetch #{upstream}\n\t" + "git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" + "\tgit remote set-head #{upstream} -a", ) end # If we don't have an upstream and an origin, we're done return if upstream == 'origin' origin_branch = main_remote_branch('origin') # NOTE: that on GL, forks don't fork any branches by default, even # a main one, so if it's 'nil', then ignore. return if origin_branch.nil? || origin_branch == upstream_branch die( "The main branch of your upstream (#{upstream_branch}) and your " + "fork/origin (#{origin_branch}) are not the same. You should go " + "to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" + 'branches/ and rename the \'default\' branch to ' + "'#{upstream_branch}'. It will then give you some commands to " + 'run to update this clone.', ) end def _forgejo_pr_opts title = git('log', '-1', '--pretty=format:%s').stdout body = git('show', '-s', '--format=%b').stdout [title, "--body=#{body}"] end end end sugarjar-4.0.0/lib/sugarjar/commands/up.rb000066400000000000000000000116451523615150300204770ustar00rootroot00000000000000class SugarJar class Commands def up(branch = nil) assert_in_repo! branch ||= current_branch branch = fprefix(branch) # get a copy of our current branch, if rebase fails, we won't # be able to determine it without backing out curr = current_branch git('checkout', branch) result = rebase if result['so'].error? backout = '' if rebase_in_progress? backout = ' You can get out of this with a `git rebase --abort`.' end die( "#{color(curr, :red)}: Failed to rebase on " + "#{result['base']}. Leaving the repo as-is.#{backout} " + 'Output from failed rebase is: ' + "\nSTDOUT:\n#{result['so'].stdout.lines.map { |x| "\t#{x}" }.join}" + "\nSTDERR:\n#{result['so'].stderr.lines.map { |x| "\t#{x}" }.join}", ) else SugarJar::Log.info( "#{color(current_branch, :green)} rebased on #{result['base']}", ) # go back to where we were if we rebased a different branch git('checkout', curr) if branch != curr end end def upall assert_in_repo! all_local_branches.each do |branch| next if MAIN_BRANCHES.include?(branch) git('checkout', branch) result = rebase if result['so'].error? SugarJar::Log.error( "#{color(branch, :red)} failed rebase. Reverting attempt and " + 'moving to next branch. Try `sj up` manually on that branch.', ) git('rebase', '--abort') if rebase_in_progress? else SugarJar::Log.info( "#{color(branch, :green)} rebased on " + color(result['base'], :green).to_s, ) end end end def fsync sync(:force => true) end alias forcesync fsync def sync(force: false) assert_in_repo! dirty_check! src = "origin/#{current_branch}" fetch('origin') want_reset = false if force SugarJar::Log.debug('Forcing reset instead of rebase at user request') want_reset = true end unless force s = git_nofail('merge-base', '--is-ancestor', 'HEAD', src) # if this IS an ancestor, we can just force reset. # # otherwise, we attempt a rebase to not lose anything (unless # force is set) if s.error? SugarJar::Log.debug( "Choosing rebase sync since this isn't a direct ancestor", ) else SugarJar::Log.debug('Choosing reset sync since this is an ancestor') want_reset = true end end if want_reset git('reset', '--hard', src) else rebase(src) s = git_nofail('rev-parse', '--verify', 'REBASE_HEAD') unless s.error? SugarJar::Log.info( 'Rebase required input. You may continue the rebase from' + ' here normally, or you may abort (`git rebase --abort`)' + ' and instead to `sj fsync` to skip a rebase and force' + ' reset to the remote branch.', ) return end end SugarJar::Log.info("Synced to #{src}.") end private def rebase(base = nil) skip_base_warning = !base.nil? SugarJar::Log.debug('Fetching upstream') fetch_upstream curr = current_branch # this isn't a hash, it's a named param, silly rubocop # rubocop:disable Style/HashSyntax base ||= tracked_branch(fallback: false) # rubocop:enable Style/HashSyntax unless base SugarJar::Log.info( 'The branch we were tracking is gone, resetting tracking to ' + most_main, ) git('branch', '-u', most_main) base = most_main end # If this is a subfeature based on a local branch which has since # been deleted, 'tracked branch' will automatically return # so we don't need any special handling for that if !MAIN_BRANCHES.include?(curr) && base == "origin/#{curr}" && !skip_base_warning SugarJar::Log.warn( "This branch is tracking origin/#{curr}, which is probably your " + 'downstream (where you push _to_) as opposed to your upstream ' + '(where you pull _from_). This means that "sj up" is probably ' + 'rebasing on the wrong thing and doing nothing. You probably want ' + "to do a 'git branch -u #{most_main}'.", ) end SugarJar::Log.debug('Rebasing') s = git_nofail('rebase', base) { 'so' => s, 'base' => base, } end def rebase_in_progress? # for rebase without -i rebase_file = git('rev-parse', '--git-path', 'rebase-apply').stdout.strip # for rebase -i rebase_merge_file = git('rev-parse', '--git-path', 'rebase-merge'). stdout.strip File.exist?(rebase_file) || File.exist?(rebase_merge_file) end end end sugarjar-4.0.0/lib/sugarjar/config.rb000066400000000000000000000056251523615150300175200ustar00rootroot00000000000000require 'yaml' require_relative 'log' require 'deep_merge' class SugarJar # This parses SugarJar configs (not to be confused with repoconfigs). # This is stuff like log level, github-user, etc. class Config DEFAULTS = { 'default_forge_host' => nil, 'pr_autofill' => true, 'pr_autostack' => nil, 'color' => true, 'ignore_deprecated_options' => [], 'host_configs' => { 'default' => { 'user' => ENV.fetch('USER'), 'use_forks' => true, }, }, }.freeze # things we have a command-line option for, but doesn't make sense # in a config file. EXTRA_CONFIGS = %w{fork_name force_forge_type}.freeze TOP_LEVEL_CONFIGS = (DEFAULTS.keys + EXTRA_CONFIGS).freeze DEPRECATED_OPTIONS = %w{ fallthru gh_cli github_user gitlab_user github_host gitlab_host }.freeze def self._find_ordered_files real_files = [] [ '/etc/sugarjar', "#{Dir.home}/.config/sugarjar", ].each do |dir| %w{yaml yml}.each do |ext| f = File.join(dir, "config.#{ext}") real_files << f if File.exist?(f) end end real_files end def self.config SugarJar::Log.debug("Defaults: #{DEFAULTS}") c = DEFAULTS.dup _find_ordered_files.each do |f| SugarJar::Log.debug("Loading config #{f}") data = YAML.safe_load_file(f) next unless data warn_on_deprecated_configs(data, f) data = data.slice(*TOP_LEVEL_CONFIGS) c.deep_merge!(data) SugarJar::Log.debug("Modified config: #{c}") end c end def self.warn_on_deprecated_configs(data, fname) # if it doesn't have host_configs **and** it has one of the # previous user options, that were effectively required before # and have now moved to host_configs, then we treat it as an # old config if %w{github_user gitlab_user}.any? { |useropt| data.key?(useropt) } && !data['host_configs'] SugarJar::Log.warn( "#{fname} appears to be an old-style config.\nIt likely will " + 'not result in the behavior you want. You should run' + "\n\n\tsj modernizeconfig #{fname}\n\n" + "To automatically convert it.\n", ) return end ignore_deprecated_options = data['ignore_deprecated_options'] || [] DEPRECATED_OPTIONS.each do |opt| next unless data.key?(opt) if ignore_deprecated_options.include?(opt) SugarJar::Log.debug( "#{fname}: Not warning about deprecated option `#{opt}` due to " + '`ignore_deprecated_options` in that file.', ) next end SugarJar::Log.warn( "#{fname}: contains deprecated option `#{opt}`. You can " + 'suppress this warning with `ignore_deprecated_options`.', ) end end end end sugarjar-4.0.0/lib/sugarjar/forge.rb000066400000000000000000000047021523615150300173500ustar00rootroot00000000000000require 'mixlib/shellout' require_relative 'util' require_relative 'log' class SugarJar class Forge attr_reader :type CMD_MAP = { 'github' => 'gh', 'gitlab' => 'glab', 'forgejo' => 'fj', }.freeze def initialize(type, host) @type = type @host = host assert_cli_avail! end def cmd @cmd ||= CMD_MAP[@type] end def all_versions CMD_MAP.values.map do |cli| bin = SugarJar::Util.which_nofail(cli) _forge_nofail(cli, 'version').stdout if bin end.compact end def run_nofail(*) _forge_nofail(cmd, *) end def run(*) s = _forge_nofail(cmd, *) s.error! s end def run_with_system(*args) SugarJar::Log.trace("Running: #{cmd} #{args.join(' ')}") system(cmd, *args) end private def _forge_nofail(cli, *args) SugarJar::Log.trace("Running: #{cli} #{args.join(' ')}") bin = SugarJar::Util.which_nofail(cli) s = Mixlib::ShellOut.new([bin] + args).run_command if s.error? && ["#{cli} auth", 'Unauthorized'].any? do |err| s.stderr.include?(err) end SugarJar::Log.info( "#{cli} was run but no gitlab token exists. Will run " + "'#{cli} auth login' to force\n#{cli} to authenticate...", ) args = if cli == 'fj' [bin, '-H', @host, 'auth', 'login'] else [bin, 'auth', 'login', '-p', 'ssh'] end unless system(*args) SugarJar::Log.fatal( "That failed, I will bail out. #{cli} needs to get a " + "token. Try running '#{cli} auth login' (will list info about " + 'your account) and try this again when that works.', ) exit(1) end if cli == 'fj' fj = Mixlib::ShellOut.new([bin, 'auth', 'list']).run_command unless fj.stdout.include?(@host) SugarJar::Log.fatal( 'Logging in for you failed - this usually means you need to ' + 'create a token manually - the instructions should have been ' + 'printed above. Setup the token and then try again.', ) exit(1) end end end s end def assert_cli_avail! return if SugarJar::Util.which_nofail(cmd) SugarJar::Log.error( "Forge CLI #{cmd} is unavailable, please install", ) exit 1 end end end sugarjar-4.0.0/lib/sugarjar/git.rb000066400000000000000000000013471523615150300170330ustar00rootroot00000000000000require_relative 'util' class SugarJar module Git def self.run_nofail(*args, color: true) if %w{diff log grep branch}.include?(args[0]) && args.none? { |x| x.include?('color') } args << (color ? '--color' : '--no-color') end SugarJar::Log.trace("Running: git #{args.join(' ')}") Mixlib::ShellOut.new([SugarJar::Util.which('git')] + args).run_command end def self.run(*, color: true) s = run_nofail(*, :color => color) s.error! s end def self.in_repo? s = run_nofail('rev-parse', '--is-inside-work-tree') !s.error? && s.stdout.strip == 'true' end def self.repo_root run('rev-parse', '--show-toplevel').stdout.strip end end end sugarjar-4.0.0/lib/sugarjar/help.rb000066400000000000000000000265771523615150300172140ustar00rootroot00000000000000class SugarJar module Help # Keyed by the "primary" command (the longest/most-descriptive name - # not necessarily the actual method name in SugarJar::Commands, though # both work identically for dispatch). `aliases` are other, usually # shorter, names that dispatch to the same command. `description` is # the full text shown by `sj help `. COMMANDS = { 'amend' => { :aliases => [], :usage => '', :description => <<~DESC, Amend the current commit. Alias for `git commit --amend`. Accepts other arguments such as `-a` or files. DESC }, 'amendq' => { :aliases => ['qamend'], :usage => '', :description => <<~DESC, Same as `amend` but without changing the message. Alias for `git commit --amend --no-edit`. DESC }, 'binfo' => { :aliases => [], :usage => '', :description => 'Verbose information about the current branch.', }, 'br' => { :aliases => [], :usage => '', :description => 'Verbose branch list. An alias for `git branch -v`.', }, 'debuginfo' => { :aliases => [], :usage => '', :description => <<~DESC, Prints out a bunch of version and config information useful for including in bug reports. DESC }, 'feature' => { :aliases => ['f'], :usage => '', :description => <<~DESC, Create a "feature" branch. It's morally equivalent to `git checkout -b ...` except it defaults to creating it based on some form of the primary branch (`master`, `main`, etc.) instead of your current branch. In order of preference it will be `upstream/$PRIMARY`, `origin/$PRIMARY`, `$PRIMARY`, depending upon what remotes are available. Note that you can specify `--feature-prefix` (or add `feature_prefix` to your config's 'host_configs' section) to have all features created with a prefix. This is useful for branch-based workflows where developers are expected to create branches names that, for example, start with their username. DESC }, 'forcepush' => { :aliases => ['fpush'], :usage => '', :description => <<~DESC, The same as `smartpush`, but uses `--force-with-lease`. This is a "safer" way of doing force-pushes and is the recommended way to push after rebasing or amending. Never do this to shared branches. Very convenient for keeping the branch behind a pull- request clean. DESC }, 'forcesync' => { :aliases => ['fsync'], :usage => '', :description => <<~DESC, Similar to `sync`, but never tries to rebase, always does a hard reset. DESC }, 'globalbranchclean' => { :aliases => ['gbclean'], :usage => '[] []', :description => <<~DESC, WARNING: EXPERIMENTAL COMMAND. Combination of `localbranchclean` and `remotebranchclean`. Cleans up both local and remote branches safely. See those commands for details. DESC }, 'globalbranchcleanall' => { :aliases => ['gbcleanall'], :usage => '[]', :description => <<~DESC, WARNING: EXPERIMENTAL COMMAND. Safely clean all branches, both local and remote. See `globalbranchclean` for details. DESC }, 'lint' => { :aliases => [], :usage => '', :description => 'Run any linters configured in the repoconfig.', }, 'localbranchclean' => { :aliases => %w{lbclean bclean}, :usage => '[]', :description => <<~DESC, If safe, delete the current branch (or the specified branch). Unlike `git branch -d`, this can handle squash-merged branches. Think of it as a smarter `git branch -d`. Aliased to `bclean` for backwards compatibility. DESC }, 'localbranchcleanall' => { :aliases => %w{lbcleanall bcleanall}, :usage => '', :description => <<~DESC, Walk all branches, and try to delete them if it's safe. See `localbranchclean` for details. Aliased to `bcleanall` for backwards compatibility. DESC }, 'modernizeconfig' => { :aliases => ['fixconfig'], :usage => '', :description => <<~DESC, Will take a sugarjar config file and modernize it for all of the changes in 4.x. You will be given a diff, and you can choose to overwrite the file, or save the new one to a new file. DESC }, 'pullsuggestions' => { :aliases => ['ps'], :usage => '', :description => <<~DESC, Pull any suggestions *that have been committed* in the GitHub UI. This will show the diff and prompt for confirmation before merging. Note that a fast-forward merge will be used. DESC }, 'remotebranchclean' => { :aliases => ['rbclean'], :usage => '[] []', :description => <<~DESC, WARNING: EXPERIMENTAL COMMAND. Similar to `localbranchclean`, except safely cleans up remote branches. Unlike many git commands, comes after so that you can specify a branch and the remote defaults to `origin`. This means you can do `sj remotebranchclean` to clean the remote branch with the same name as the local one. Note that you probably want `globalbranchclean`, which will do both local and remote cleaning in one command. WARNING: This command cannot differentiate release branches that are fully merged but still need to be kept around for future work unless they are specified in your repoconfig! So if main contains everything that 2.0-devel and 3.0-devel has, then those branches will be deleted. Use with caution. DESC }, 'remotebranchcleanall' => { :aliases => ['rbcleanall'], :usage => '[]', :description => <<~DESC, WARNING: EXPERIMENTAL COMMAND. Walk all remote branches, and try to delete them if it's safe. See `remotebranchclean` for details. DESC }, 'smartclone' => { :aliases => ['sclone'], :usage => ' []', :description => <<~DESC, A smart wrapper to `git clone` that handles forking and managing remotes for you. If the org of the repository is not the same as your forge user then it will fork the repo for you to your account (if not already done), clone the repo, and then setup your remotes so that `origin` is your fork and `upstream` is the upstream. It is assumed that there will be at least one positional argument, and it will be the repo in any format (git, ssh, forge-style shortname [e.g. e.g. `$org/$repo`]). A second positional argument will be interpreted as the directory to clone into. If you want to change the name of the repo in your fork of it, you may pass in `--fork-name` to specify another. Note that if you pass in additional options after " -- ", they will be passed to `gh` in the case of GitHub, or `git` in the case of GitLab. For example to clone foo/bar/docs on gitlab, but have the repo named "bar-docs" when it's cloned to your org, and to have the directory be called "bar-docs": sj sclone foo/bar/docs bar-docs \\ --default-forge-host gitlab.com --fork-name bar-docs Or for GitHub: sj sclone bar/docs bar-docs \\ --default-forge-host github.com --fork-name bar-docs DESC }, 'smartlog' => { :aliases => ['sl'], :usage => '', :description => <<~DESC, Inspired by Facebook's `sl` extension to Mercurial, this command will show you a tree of all your local branches relative to your upstream. DESC }, 'smartpullrequest' => { :aliases => %w{smartpr spr}, :usage => '', :description => <<~DESC, A smart wrapper to `gh pr`/`glab mr`/etc. that checks if your repo is dirty before creating the pull request, handles intelligently picking the base, fills in the PR body, etc. DESC }, 'smartpush' => { :aliases => ['spush'], :usage => '', :description => <<~DESC, A smart wrapper to `git push` that runs whatever is defined in `on_push` in the repoconfig, and only pushes if they succeed. DESC }, 'subfeature' => { :aliases => ['sf'], :usage => '', :description => 'An alias for `sj feature `.', }, 'sync' => { :aliases => [], :usage => '', :description => <<~DESC, Similar to `up`, except instead of rebasing on a tracked branch (usually `upstream` remote), rebases to wherever our remote push target is (usually `origin` remote). Useful for syncing work across different machines. For example, if you do some work on feature `foo` on machine1 and push to `origin/foo` (intending to eventually merge to `upstream/main`), then on machine2, you pull that branch, do more work, which you also push to `origin/foo`, then on machine1, you can do `sj sync` to pull down the changes from `origin/foo`. If you have local changes, that are not already on `origin/foo`, those will be rebased on top of the changes from `origin/foo`. DESC }, 'unit' => { :aliases => [], :usage => '', :description => 'Run any unitests configured in the repoconfig.', }, 'up' => { :aliases => [], :usage => '[]', :description => <<~DESC, Rebase the current branch (or specified branch) intelligently. In most causes this will check for a primary branch on `upstream`, then `origin`. If a branch explicitly tracks something else, then that will be used, instead. DESC }, 'upall' => { :aliases => [], :usage => '', :description => 'Same as `up`, but for all branches.', }, }.freeze # Map of every valid command/alias name -> its canonical (primary) name. ALIASES = COMMANDS.each_with_object({}) do |(name, info), map| map[name] = name info[:aliases].each { |a| map[a] = name } end.freeze def self.canonical(name) ALIASES[name] end def self.summary_list COMMANDS.sort.map do |name, info| names = ([name] + info[:aliases]).join(', ') " #{names}" end.join("\n") end def self.command_help(name) canonical_name = canonical(name) return nil unless canonical_name info = COMMANDS[canonical_name] names = ([canonical_name] + info[:aliases]).join(', ') usage = info[:usage].empty? ? '' : " #{info[:usage]}" "#{names}#{usage}\n\n#{info[:description]}" end end end sugarjar-4.0.0/lib/sugarjar/log.rb000066400000000000000000000010341523615150300170220ustar00rootroot00000000000000require 'mixlib/log' # rubocop:disable Style/OneClassPerFile module Mixlib module Log # A simple formatter so that 'info' is just like 'puts' # but everything else gets a severity class Formatter def call(severity, _time, _progname, msg) if severity == 'INFO' "#{msg2str(msg)}\n" else "#{severity}: #{msg2str(msg)}\n" end end end end end class SugarJar # Our singleton logger class Log extend Mixlib::Log end end # rubocop:enable Style/OneClassPerFile sugarjar-4.0.0/lib/sugarjar/repoconfig.rb000066400000000000000000000027401523615150300204010ustar00rootroot00000000000000require_relative 'git' require_relative 'log' require 'yaml' require 'deep_merge' class SugarJar # This parses SugarJar repoconfigs (not to be confused with configs). # This is lint/unit/on_push configs. class RepoConfig CONFIG_NAME = '.sugarjar.yaml'.freeze def self.repo_config_path(config) ::File.join(SugarJar::Git.repo_root, config) end def self.hash_from_file(config_file) SugarJar::Log.debug("Loading repo config: #{config_file}") YAML.safe_load_file(config_file) end # wrapper for File.exist to make unittests easier def self.config_file?(config_file) File.exist?(config_file) end def self.config(config = CONFIG_NAME) data = {} unless SugarJar::Git.in_repo? SugarJar::Log.debug('Not in repo, skipping repoconfig load') return data end config_file = repo_config_path(config) data = hash_from_file(config_file) if config_file?(config_file) if data['overwrite_from'] && config_file?(data['overwrite_from']) SugarJar::Log.debug( "Attempting overwrite_from #{data['overwrite_from']}", ) data = config(data['overwrite_from']) data.delete('overwrite_from') elsif data['include_from'] && config_file?(data['include_from']) SugarJar::Log.debug("Attempting include_from #{data['include_from']}") data.deep_merge!(config(data['include_from'])) data.delete('include_from') end data end end end sugarjar-4.0.0/lib/sugarjar/util.rb000066400000000000000000000015661523615150300172300ustar00rootroot00000000000000require 'mixlib/shellout' require_relative 'log' class SugarJar module Util # a mixin to hold stuff that Commands and RepoConfig both use def self.which(cmd) path = which_nofail(cmd) return path if path SugarJar::Log.fatal("Could not find #{cmd} in your path") exit(1) end # Finds the first entry in the path for a binary and checks # to make sure it's not us. Warn if it is us as that won't work in 2.x def self.which_nofail(cmd) ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| p = File.join(dir, cmd) next unless File.exist?(p) && File.executable?(p) if File.basename(File.realpath(p)) == 'sj' SugarJar::Log.error( "'#{cmd}' is linked to 'sj' which is no longer supported.", ) next end return p end false end end end sugarjar-4.0.0/lib/sugarjar/version.rb000066400000000000000000000000561523615150300177310ustar00rootroot00000000000000class SugarJar VERSION = '4.0.0'.freeze end sugarjar-4.0.0/packaging/000077500000000000000000000000001523615150300152565ustar00rootroot00000000000000sugarjar-4.0.0/packaging/README-brew.md000066400000000000000000000014211523615150300174700ustar00rootroot00000000000000# Homebrew Packaging Notes NOTE: Homebrew now auto-updates on release, so this should mostly be unneeded. ## Prep PR * Get latest brew sources, `brew update` * Edit `/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/s/sugarjar.rb` modifying the version and the sha. See [previous example](https://github.com/Homebrew/homebrew-core/pull/162477) * Commit, make the title "sugarjar $VERSION" ## Test Do a install from source: ```shell HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source sugarjar ``` Then test: ```shell brew test sugarjar brew audit --strict sugarjar ``` ## Make PR The real upstream has to be called `origin` in Homebrew, so push to our forked remote: ```shell git push jaymzh ``` And make the PR from the webUI. sugarjar-4.0.0/packaging/README-fedora.md000066400000000000000000000043111523615150300177720ustar00rootroot00000000000000# Fedora Packaging Notes This is mostly notes to myself. ## Refs Some links and refs useful to keep handy * [Sugar Jar dist-git](https://src.fedoraproject.org/rpms/rubygem-sugarjar) * [Package Maintenance Guide]( https://docs.fedoraproject.org/en-US/package-maintainers/Package_Maintenance_Guide/ ) * [Machines you can use]( https://fedoraproject.org/wiki/Test_Machine_Resources_For_Package_Maintainers ) ## Prep Use fedora distrobox. If not already checked out, check out the dist-git: ```shell fedpkg co rubygem-sugarjar ``` Make sure you start on the 'rawhide' branch. If already checked out, do `fedpkg pull` to get the latest. ## Do work Make whatever changes you want on rawhide. If you're doing a version bump you'll need to grab both new sources and replace the old ones. First follow the directions in the spec file to build the tarball for the test files. Then wget the gem from the URL in the spec file. Then: ```shell fedpkg new-sources rubygem-sugarjar--specs.tar sugarjar-.gem ``` ## Testing You can do a local build (`fedpkg local`) or a mock build (`fedpkg mockbuild`). You can, alternatively, submit a koji build: ```shell # build a SRPM fedpkg srpm # make sure your krb-auth'd krb # Submit the koji build koji build --scratch rawhide ``` ## Committing and pushing First, commit your change: ```shell fedpkg commit ``` You can push directly to master if you want (`fedpkg push`), or alternatively, make a PR by adding your remote: ```shell git remote add fork ssh://jaymzh@pkgs.fedoraproject.org/forks/jaymzh/rpms/rubygem-sugarjar.git ``` And push to that instead (`git push fork`), and click the link to make a PR. Once it's pushed/merged, you can create a build: ```shell fedpkg build ``` For Rawhide, if the build succeeds, you're done. To build for other distros, switch branches with: ```shell fedpkg switch-branch ``` And you can just merge in rawhide (`git merge rawhide`), then build. For non-rawhide branches, after the `build`, submit the update: ```shell fedpkg update ``` That will push it to testing. Autokarma should push it to stable after about a week (though you can manually push it with `bodhi updates request stable`). sugarjar-4.0.0/packaging/Vagrantfile000066400000000000000000000017141523615150300174460ustar00rootroot00000000000000Vagrant.configure('2') do |config| # Libvirt/KVM VM (Fedora 41) config.vm.define 'f41' do |f41| f41.vm.provider :libvirt do |libvirt| libvirt.memory = 2048 libvirt.cpus = 2 end f41.vm.box = 'fedora/41-cloud-base' f41.vm.provision :shell, :path => 'provision.sh' f41.vm.synced_folder '..', '/home/vagrant/sugarjar', :type => 'nfs', :nfs_version => 4 f41.vm.synced_folder '../../pastel', '/home/vagrant/pastel', :type => 'nfs', :nfs_version => 4 f41.vm.synced_folder '../../tty-color', '/home/vagrant/tty-color', :type => 'nfs', :nfs_version => 4 f41.vm.synced_folder '../../mixlib-shellout', '/home/vagrant/mixlib-shellout', :type => 'nfs', :nfs_version => 4 f41.ssh.insert_key = false end config.ssh.extra_args = [ '-o', 'IdentitiesOnly=yes', '-o', 'GSSAPIAuthentication=no' ] end sugarjar-4.0.0/packaging/provision.sh000077500000000000000000000010641523615150300176460ustar00rootroot00000000000000#!/bin/bash sudo dnf install fedora-packager fedora-review rubygems-devel \ rubygem-rspec rubygem-gem2rpm git vim -y sudo usermod -a -G mock vagrant newgrp echo 'jaymzh' > ~vagrant/.fedora.upn mkdir ~vagrant/bin cat > ~vagrant/bin/krb < ~vagrant/.gitconfig <> ~vagrant/.bashrc <<'EOF' source /usr/share/git-core/contrib/completion/git-prompt.sh export PS1="[\u@\h\$(__git_ps1) \W]\$ " export EDITOR=vim EOF sugarjar-4.0.0/rubygem-sugarjar.spec000066400000000000000000000045171523615150300175030ustar00rootroot00000000000000# tests won't work until dependent packages are available %bcond_without tests %global app_root %{_datadir}/%{name} %global gem_name sugarjar %global version 0.0.10 %global release 1 %global common_description %{expand: Sugarjar is a utility to help making working with git and GitHub easier. In particular it has a lot of features to make rebase-based and squash-based workflows simpler.} Name: rubygem-%{gem_name} Summary: A git/github helper utility Version: %{version} Release: %{release}%{?dist} License: ASL 2.0 URL: http://www.github.com/jaymzh/sugarjar BuildRequires: rubygems-devel BuildRequires: rubygem-mixlib-shellout %if %{with tests} BuildRequires: rubygem-rspec BuildRequires: rubygem-mixlib-log BuildRequires: hub %endif Requires: hub Requires: git-core BuildArch: noarch Source0: https://rubygems.org/downloads/%{gem_name}-%{version}.gem # git clone https://github.com/jaymzh/sugarjar.git # git checkout v0.0.10 # tar -cf rubygem-sugarjar-0.0.10-specs.tar.gz spec/ Source1: %{name}-%{version}-specs.tar.gz %description %{common_description} %package -n sugarjar Summary: A git/github helper utility Requires: hub, git %description -n sugarjar %{common_description} %prep %setup -q -n %{gem_name}-%{version} -b 1 %build gem build ../%{gem_name}-%{version}.gemspec %gem_install %install mkdir -p %{buildroot}%{gem_dir} cp -a ./%{gem_dir}/* %{buildroot}%{gem_dir}/ mkdir -p %{buildroot}%{_bindir} cp -a ./%{_bindir}/* %{buildroot}%{_bindir} find %{buildroot}%{gem_instdir}/bin -type f | xargs chmod a+x %if %{with tests} %check cd .. ln -s sugarjar-%{version}/lib . find rspec spec %endif %clean rm -rf %{buildroot} %files -n sugarjar %dir %{gem_instdir} %{_bindir}/sj %{gem_instdir}/bin %license %{gem_instdir}/LICENSE %doc %{gem_instdir}/README.md %{gem_libdir} %exclude %{gem_cache} %exclude %{gem_instdir}/{Gemfile,sugarjar.gemspec} # We don't have ri/rdoc in our sources %exclude %{gem_docdir} %{gem_spec} %changelog * Tue Aug 23 2022 Phil Dibowitz - 0.0.10-1 - Update to upstream 0.0.10 * Mon Mar 08 2021 Phil Dibowitz - 0.0.9-3 - Add rspec BuildRequires for tests * Mon Mar 01 2021 Phil Dibowitz - 0.0.9-2 - Use global instead of define - Mark the license as a license - Re-enable tests now that rubygem-mixlib-log exists * Sun Feb 28 2021 Phil Dibowitz - 0.0.9-1 - Initial package sugarjar-4.0.0/scripts/000077500000000000000000000000001523615150300150215ustar00rootroot00000000000000sugarjar-4.0.0/scripts/get_linters000077500000000000000000000000741523615150300172670ustar00rootroot00000000000000echo "scripts/run_rubocop.sh -D -a scripts/run_mdl.sh -g ." sugarjar-4.0.0/scripts/run_mdl.sh000077500000000000000000000004431523615150300170210ustar00rootroot00000000000000#!/bin/bash SCRIPTS=$(dirname "$(realpath "$0")") REPODIR="$SCRIPTS/.." BIN=$(type mdl | awk '{print $NF}') if [ -n "$1" ]; then args=( "$@" ) else cd "$REPODIR" || { echo "Failed to cd to repo root"; exit 1; } args=('.') fi # shellcheck disable=SC2086 exec $BIN "${args[@]}" sugarjar-4.0.0/scripts/run_rspec.sh000077500000000000000000000005011523615150300173540ustar00rootroot00000000000000#!/bin/bash SCRIPTS=$(dirname "$(realpath "$0")") REPODIR="$SCRIPTS/.." BIN='bundle exec rspec' CMD="$BIN --format d" if [ -n "$1" ]; then args=( "$@" ) else cd "$REPODIR" || { echo "Failed to cd to repo root"; exit 1; } args=() fi # shellcheck disable=SC2086 echo $CMD "${args[@]}" exec $CMD "${args[@]}" sugarjar-4.0.0/scripts/run_rubocop.sh000077500000000000000000000005141523615150300177150ustar00rootroot00000000000000#!/bin/bash SCRIPTS=$(dirname "$(realpath "$0")") REPODIR="$SCRIPTS/.." BIN='bundle exec rubocop' CMD="$BIN --display-cop-names" if [ -n "$1" ]; then args=( "$@" ) else cd "$REPODIR" || { echo "Failed to cd to repo root"; exit 1; } args=() fi # shellcheck disable=SC2086 echo $CMD "${args[@]}" exec $CMD "${args[@]}" sugarjar-4.0.0/spec/000077500000000000000000000000001523615150300142645ustar00rootroot00000000000000sugarjar-4.0.0/spec/commands/000077500000000000000000000000001523615150300160655ustar00rootroot00000000000000sugarjar-4.0.0/spec/commands/amend_spec.rb000066400000000000000000000031471523615150300205150ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true, 'github_user' => 'myuser' }) end context '#amend' do it 'calls git ammend properly when no additional args are passed' do git = '/usr/bin/git' allow(sj).to receive(:assert_in_repo!) allow(sj).to receive(:which).with('git').and_return(git) expect(sj).to receive(:system).with(git, 'commit', '--amend'). and_return(true) expect(sj).to receive(:exit).with(true) sj.amend end it 'calls git ammend with additional args' do git = '/usr/bin/git' allow(sj).to receive(:assert_in_repo!) allow(sj).to receive(:which).with('git').and_return(git) expect(sj).to receive(:system).with(git, 'commit', '--amend', '-s'). and_return(true) expect(sj).to receive(:exit).with(true) sj.amend('-s') end end context '#qamend' do it 'calls git ammend properly when no additional args are passed' do allow(sj).to receive(:assert_in_repo!) so = double({ 'stdout' => 'some output' }) expect(sj).to receive(:git).with('commit', '--amend', '--no-edit'). and_return(so) sj.qamend end it 'calls git ammend with additional args' do git = '/usr/bin/git' allow(sj).to receive(:assert_in_repo!) allow(sj).to receive(:which).with('git').and_return(git) so = double({ 'stdout' => 'some output' }) expect(sj).to receive(:git).with('commit', '--amend', '--no-edit', '-s'). and_return(so) sj.qamend('-s') end end end sugarjar-4.0.0/spec/commands/bclean_spec.rb000066400000000000000000000101351523615150300206500ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end context '#safe_to_clean?' do it 'Allows cleanup when cherry -v shows no delta' do expect(sj).to receive(:tracked_branch).with('foo'). and_return('origin/main') so = double({ 'stdout' => '' }) expect(sj).to receive(:git).with('cherry', '-v', 'origin/main', 'foo'). and_return(so) expect(sj.send(:safe_to_clean?, 'foo')).to eq(true) end it 'Allows cleanup when cherry -v shows no important delta' do expect(sj).to receive(:tracked_branch).with('foo'). and_return('origin/main') so = double({ 'stdout' => "- aabbcc0 something\n-bbccdd1 another\n" }) expect(sj).to receive(:git).with('cherry', '-v', 'origin/main', 'foo'). and_return(so) expect(sj.send(:safe_to_clean?, 'foo')).to eq(true) end it 'Does not allow cleanup when we fail to build our merge test branch' do branch = 'foo' tracked_branch = 'origin/main' tmp_branch = '_sugar_jar.123' expect(sj).to receive(:tracked_branch).and_return(tracked_branch) so = double({ 'stdout' => "+ aabbcc0 something\n" }) expect(sj).to receive(:git).with('cherry', '-v', tracked_branch, branch). and_return(so) expect(Process).to receive(:pid).and_return(123) expect(sj).to receive(:git).with( 'checkout', '-b', tmp_branch, tracked_branch ) so2 = double({ 'error?' => true }) expect(sj).to receive(:git_nofail).with('merge', '--squash', branch). and_return(so2) expect(sj).to receive(:cleanup_tmp_branch). with(tmp_branch, branch, tracked_branch) expect(sj.send(:safe_to_clean?, branch)).to eq(false) end it 'Does not allow cleanup when merge test branch shows delta' do branch = 'foo' tracked_branch = 'origin/main' tmp_branch = '_sugar_jar.123' expect(sj).to receive(:tracked_branch).and_return(tracked_branch) so = double({ 'stdout' => "+ aabbcc0 something\n" }) expect(sj).to receive(:git).with('cherry', '-v', tracked_branch, branch). and_return(so) expect(Process).to receive(:pid).and_return(123) expect(sj).to receive(:git).with( 'checkout', '-b', tmp_branch, tracked_branch ) so2 = double({ 'error?' => false }) expect(sj).to receive(:git_nofail).with('merge', '--squash', branch). and_return(so2) so3 = double({ 'stdout' => 'here is output' }) expect(sj).to receive(:git).with('diff', '--staged').and_return(so3) expect(sj).to receive(:cleanup_tmp_branch). with(tmp_branch, branch, tracked_branch) expect(sj.send(:safe_to_clean?, branch)).to eq(false) end it 'Does allows cleanup when merge test branch shows no delta' do branch = 'foo' tracked_branch = 'origin/main' tmp_branch = '_sugar_jar.123' expect(sj).to receive(:tracked_branch).and_return(tracked_branch) so = double({ 'stdout' => "+ aabbcc0 something\n" }) expect(sj).to receive(:git).with('cherry', '-v', tracked_branch, branch). and_return(so) expect(Process).to receive(:pid).and_return(123) expect(sj).to receive(:git).with( 'checkout', '-b', tmp_branch, tracked_branch ) so2 = double({ 'error?' => false }) expect(sj).to receive(:git_nofail).with('merge', '--squash', branch). and_return(so2) so3 = double({ 'stdout' => '' }) expect(sj).to receive(:git).with('diff', '--staged').and_return(so3) expect(sj).to receive(:cleanup_tmp_branch). with(tmp_branch, branch, tracked_branch) expect(sj.send(:safe_to_clean?, branch)).to eq(true) end it 'Uses the correct base for detecting delta' do expect(sj).to receive(:tracked_branch).with('feature/foo'). and_return('origin/develop') so = double({ 'stdout' => '' }) expect(sj).to receive(:git). with('cherry', '-v', 'origin/develop', 'feature/foo'). and_return(so) expect(sj.send(:safe_to_clean?, 'feature/foo')).to eq(true) end end end sugarjar-4.0.0/spec/commands/branch_spec.rb000066400000000000000000000035611523615150300206660ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end context '#checkout' do it 'attempts to checkout the branch with no feature prefix' do branch = 'foo' allow(sj).to receive(:assert_in_repo!) expect(sj).to receive(:all_local_branches).and_return(['main', branch]) so = double({ 'stdout' => '', 'stderr' => '' }) expect(sj).to receive(:git).with('checkout', branch).and_return(so) sj.checkout(branch) end it 'attempts to checkout the branch with feature prefix, if it exists' do sj = SugarJar::Commands.new( { 'no_change' => true, 'host_configs' => { 'default' => { 'feature_prefix' => 'fp/', }, }, }, ) branch = 'foo' allow(sj).to receive(:assert_in_repo!) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main', "fp/#{branch}"]) so = double({ 'stdout' => '', 'stderr' => '' }) expect(sj).to receive(:git).with('checkout', "fp/#{branch}").and_return(so) sj.checkout(branch) end it 'will checkout non-prefixed branch if prefixed branch does not exist' do sj = SugarJar::Commands.new( { 'no_change' => true, 'host_configs' => { 'default' => { 'feature_prefix' => 'fp/', }, }, }, ) branch = 'foo' allow(sj).to receive(:assert_in_repo!) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main', branch]) so = double({ 'stdout' => '', 'stderr' => '' }) expect(sj).to receive(:git).with('checkout', branch).and_return(so) sj.checkout(branch) end end end sugarjar-4.0.0/spec/commands/checks_spec.rb000066400000000000000000000210741523615150300206700ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do context '#get_checks_from_command' do it 'returns nil if no list_cmd exists' do expect(SugarJar::RepoConfig).to receive(:config).and_return({}) sj = SugarJar::Commands.new({ 'no_change' => true }) expect(sj.get_checks_from_command('lint')).to eq(nil) expect(sj.get_checks_from_command('unit')).to eq(nil) end context 'runs the commands if they exist and returns the results' do %w{yaml bare}.each do |outtype| it "with #{outtype} output" do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'lint_list_cmd' => 'get_lint_commands', 'unit_list_cmd' => 'get_unit_commands', }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| checks = %w{one two}.map { |c| "#{type}_#{c}" } case outtype when 'yaml' out = "---\n#{ checks.map do |c| "- name: #{c}\n command: #{c}_script.sh" end.join("\n") }" when 'bare' out = checks.join("\n") + "\n" end cmd = "get_#{type}_commands" expect(File).to receive(:exist?).with(cmd).and_return(true) so = double( { :error? => false, :stdout => out, }, ) expect(Mixlib::ShellOut).to receive(:new).and_return(so) expect(so).to receive(:run_command).and_return(so) case outtype when 'yaml' expect(sj.get_checks_from_command(type)).to eq( [ { 'name' => "#{type}_one", 'command' => "#{type}_one_script.sh", }, { 'name' => "#{type}_two", 'command' => "#{type}_two_script.sh", }, ], ) when 'bare' expect(sj.get_checks_from_command(type)). to eq(["#{type}_one", "#{type}_two"]) end end end end end end context '#get_checks' do it 'defaults to _cmd variety' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'lint_list_cmd' => 'get_lint_commands', 'unit_list_cmd' => 'get_unit_commands', 'lint' => ['lint_foo'], 'unit' => ['unit_foo'], }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| expect(sj).to receive(:get_checks_from_command).with(type). and_return([ "#{type}_cmd1", "#{type}_cmd2" ]) expect(sj.get_checks(type)). to eq(["#{type}_cmd1", "#{type}_cmd2"]) end end it 'returns false if _cmd does not exist' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'lint_list_cmd' => 'get_lint_commands', 'unit_list_cmd' => 'get_unit_commands', 'lint' => ['lint_foo'], 'unit' => ['unit_foo'], }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| cmd = "get_#{type}_commands" expect(File).to receive(:exist?).with(cmd).and_return(false) expect(sj.get_checks(type)).to eq(false) end end it 'returns false if _cmd fails' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'lint_list_cmd' => 'get_lint_commands', 'unit_list_cmd' => 'get_unit_commands', 'lint' => ['lint_foo'], 'unit' => ['unit_foo'], }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| cmd = "get_#{type}_commands" expect(File).to receive(:exist?).with(cmd).and_return(true) so = double({ :error? => true, :format_for_exception => 'error' }) expect(Mixlib::ShellOut).to receive(:new).with(cmd).and_return(so) expect(so).to receive(:run_command).and_return(so) expect(sj.get_checks(type)).to eq(false) end end it 'uses static configs if no _cmd variety' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'lint' => ['lint_foo'], 'unit' => ['unit_foo'], }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| expect(sj.get_checks(type)).to eq(["#{type}_foo"]) end end end context '#run_check' do it 'amends diff if linter autocorrects and user says yes' do sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:repo_root).and_return('root') expect(Dir).to receive(:chdir).with('root').and_yield expect(sj).to receive(:get_checks).with('lint'). and_return(['lint_foo']) expect(SugarJar::Util).to receive(:which_nofail).with('lint_foo'). exactly(2).times.and_return('lint_foo') so = double({ :stdout => 'some lint output', :error? => false }) expect(Mixlib::ShellOut).to receive(:new).exactly(2).time. with('lint_foo').and_return(so) expect(so).to receive(:run_command).exactly(2).times.and_return(so) expect(sj).to receive(:dirty?).and_return(true) so2 = double({ 'stdout' => 'some diff output' }) expect(sj).to receive(:git).with('diff').and_return(so2) expect($stdout).to receive(:print) expect($stdin).to receive(:gets).and_return("a\n") expect(sj).to receive(:qamend).with('-a') expect(sj).to receive(:dirty?).and_return(false) sj.run_check('lint', true) end it 'quits if linter autocorrects and user says no' do sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:repo_root).and_return('root') expect(Dir).to receive(:chdir).with('root').and_yield expect(sj).to receive(:get_checks).with('lint'). and_return(['lint_foo']) expect(SugarJar::Util).to receive(:which_nofail).with('lint_foo'). and_return('lint_foo') so = double({ :stdout => 'some lint output', :error? => false }) expect(Mixlib::ShellOut).to receive(:new).with('lint_foo'). and_return(so) expect(so).to receive(:run_command).and_return(so) expect(sj).to receive(:dirty?).and_return(true) so2 = double({ 'stdout' => 'some diff output' }) expect(sj).to receive(:git).with('diff').and_return(so2) expect($stdout).to receive(:print) expect($stdin).to receive(:gets).and_return("q\n") expect(sj).to receive(:exit).with(1) do raise SystemExit, 1 end expect do sj.run_check('lint', true) end.to raise_error(SystemExit) end it 'returns false if the check fails' do sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| cmd = "#{type}_foo" expect(SugarJar::Git).to receive(:repo_root).and_return('root') expect(Dir).to receive(:chdir).with('root').and_yield expect(sj).to receive(:get_checks).with(type).and_return([cmd]) expect(SugarJar::Util).to receive(:which_nofail).with(cmd). and_return(cmd) so = double( { :stdout => '', :error? => true, :format_for_exception => '' }, ) expect(Mixlib::ShellOut).to receive(:new).with(cmd).and_return(so) expect(so).to receive(:run_command).and_return(so) expect(sj).to receive(:dirty?).and_return(false) if type == 'lint' expect(sj.run_check(type, true)).to eq(false) end end it 'handles hash format checks' do sj = SugarJar::Commands.new({ 'no_change' => true }) %w{lint unit}.each do |type| name = "#{type}_foo" cmd = "#{name}.sh" expect(SugarJar::Git).to receive(:repo_root).and_return('root') expect(Dir).to receive(:chdir).with('root').and_yield expect(sj).to receive(:get_checks).with(type). and_return([{ 'name' => name, 'command' => cmd }]) expect(SugarJar::Util).to receive(:which_nofail).with(cmd). and_return("/some/path/#{cmd}") so = double({ :stdout => 'some output', :error? => false }) expect(Mixlib::ShellOut).to receive(:new). with(cmd).and_return(so) expect(so).to receive(:run_command).and_return(so) expect(sj).to receive(:dirty?).and_return(false) if type == 'lint' sj.run_check(type, true) end end end end sugarjar-4.0.0/spec/commands/feature_spec.rb000066400000000000000000000071011523615150300210560ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end before(:each) do expect(sj).to receive(:assert_in_repo!).and_return(true) end context '#feature' do context 'with no specified base' do it 'creates a branch based on remote-most-main-branch with no args' do branch = 'foo' expect(sj).to receive(:fprefix).with(branch).and_return(branch) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main']) expect(sj).to receive(:all_remotes).and_return(%w{upstream origin}) expect(sj).to receive(:git).with('fetch', 'upstream') expect(sj).to receive(:git).with( 'checkout', '-b', branch, 'upstream/main' ) expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') sj.feature(branch) end it 'checks out a release branch properly' do branch = 'v2-branch' upstream_release = 'upstream/v2-branch' expect(sj).to receive(:all_remotes).and_return(%w{origin upstream}) expect(sj).to receive(:fprefix).with(branch).and_return(branch) expect(sj).to receive(:release_branches).and_return(['v2-branch']) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main']) expect(sj).to receive(:git).with('fetch', 'upstream') expect(sj).to receive(:git). with('checkout', '-b', branch, upstream_release) expect(sj).to receive(:git).with('branch', '-u', upstream_release) sj.feature(branch) end end context 'with specified base' do it 'creates a branch based on requested local branch with args' do branch = 'foo' base = 'bar' expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main', base]) expect(sj).to receive(:fprefix).with(branch).and_return(branch) expect(sj).to receive(:fprefix).with(base).and_return(base) expect(sj).to receive(:git).with('checkout', '-b', branch, base) expect(sj).to receive(:git).with('branch', '-u', base) sj.feature(branch, base) end it 'creates a branch based on requested remote branch with args' do branch = 'foo' base = 'upstream/bar' expect(sj).to receive(:fprefix).with(branch).and_return(branch) expect(sj).to receive(:fprefix).with(base).and_return(base) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main']) expect(sj).to receive(:git).with('fetch', 'upstream') expect(sj).to receive(:git).with('checkout', '-b', branch, base) expect(sj).to receive(:git).with('branch', '-u', base) sj.feature(branch, base) end it 'creates a subfeature based on the proper upstream release' do base = 'v2-branch' upstream_release = 'upstream/v2-branch' branch = 'my-v2-work' expect(sj).to receive(:all_remotes).and_return(%w{origin upstream}) expect(sj).to receive(:fprefix).with(branch).and_return(branch) expect(sj).to receive(:release_branches).and_return(['v2-branch']) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(['main']) expect(sj).to receive(:git).with('fetch', 'upstream') expect(sj).to receive(:git). with('checkout', '-b', branch, upstream_release) expect(sj).to receive(:git).with('branch', '-u', upstream_release) sj.feature(branch, base) end end end end sugarjar-4.0.0/spec/commands/modernize_config_spec.rb000066400000000000000000000106041523615150300227460ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' require_relative '../../lib/sugarjar/config' describe 'Sugarjar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end context '#_modernize' do it 'moves host-config defaults from top-level configs' do old = { 'use_forks' => false, 'feature_prefix' => 'stuff', } expected = { 'host_configs' => { 'default' => old.dup, }, } new = sj.send(:_modernize, old) expect(new).to eq(expected) # explicitly check these too, even though the above should catch it expect(new).not_to have_key('use_forks') expect(new).not_to have_key('feature_prefix') end it 'does not overwrite existing bool defaults from old top-level configs' do old = { 'use_forks' => false, 'feature_prefix' => 'stuff', 'host_configs' => { 'default' => { 'use_forks' => true, }, }, } expected = { 'host_configs' => { 'default' => { # don't overrite new one 'use_forks' => true, # set one not previously set 'feature_prefix' => 'stuff', }, }, } new = sj.send(:_modernize, old) expect(new).to eq(expected) end it 'does not overwrite existing str defaults from old top-level configs' do old = { 'use_forks' => false, 'feature_prefix' => 'stuff', 'host_configs' => { 'default' => { 'feature_prefix' => 'newstuff', }, }, } expected = { 'host_configs' => { 'default' => { 'use_forks' => false, # do not overwrite already-set thing 'feature_prefix' => 'newstuff', }, }, } new = sj.send(:_modernize, old) expect(new).to eq(expected) end it 'moves top-level forge users to their default hosts' do old = { 'github_user' => 'ghuser', 'gitlab_user' => 'gluser', } expected = { 'host_configs' => { 'default' => {}, 'github.com' => { 'user' => 'ghuser', }, 'gitlab.com' => { 'user' => 'gluser', }, }, } new = sj.send(:_modernize, old) expect(new).to eq(expected) end it 'does not overwrite exisiting users' do old = { 'github_user' => 'ghuser', 'gitlab_user' => 'gluser', 'host_configs' => { 'github.com' => { 'user' => 'anotheruser', }, }, } expected = { 'host_configs' => { 'default' => {}, 'github.com' => { 'user' => 'anotheruser', }, 'gitlab.com' => { 'user' => 'gluser', }, }, } new = sj.send(:_modernize, old) expect(new).to eq(expected) end it 'populates default users into non-default hosts with no user' do old = { 'github_user' => 'ghuser', 'gitlab_user' => 'gluser', 'host_configs' => { 'gitlab.company.com' => { 'use_forks' => true, }, 'github.example.com' => { 'feature_prefix' => 'something/', }, 'github.anothercompany.com' => { 'user' => 'specificuser', }, 'gitlab.sample.com' => { 'user' => 'anotherspecificuser', }, }, } expected = { 'host_configs' => { 'default' => {}, # always populate defaults if not set 'github.com' => { 'user' => 'ghuser', }, 'gitlab.com' => { 'user' => 'gluser', }, # these are populated, they weren't set 'gitlab.company.com' => { 'use_forks' => true, 'user' => 'gluser', }, 'github.example.com' => { 'feature_prefix' => 'something/', 'user' => 'ghuser', }, # these don't get set, they were already set 'github.anothercompany.com' => { 'user' => 'specificuser', }, 'gitlab.sample.com' => { 'user' => 'anotherspecificuser', }, }, } new = sj.send(:_modernize, old) expect(new).to eq(expected) end end end sugarjar-4.0.0/spec/commands/smartclone_spec.rb000066400000000000000000000157471523615150300216110ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:opts) do { 'no_change' => true, 'github_user' => 'myuser', 'use_forks' => true } end context '#smartclone' do let(:sj) do SugarJar::Commands.new(opts) end let(:forge_type) { 'github' } let(:forge_double) do instance_double(SugarJar::Forge, :type => forge_type) end before do allow(SugarJar::Git).to receive(:in_repo?).and_return(false) allow(sj).to receive(:forge).and_return(forge_double) end context 'repo is in our own org' do let(:repo) do 'git@github.com:myuser/repo.git' end it 'uses git' do expect(forge_double).to_not receive(:run) expect(forge_double).to_not receive(:run_nofail) expect(sj).to receive(:git).with('clone', repo, 'repo') sj.smartclone(repo) end it 'passes additional arguments to git' do expect(forge_double).to_not receive(:run) expect(forge_double).to_not receive(:run_nofail) expect(sj).to receive(:git).with('clone', repo, 'somedir', '--something') sj.smartclone(repo, 'somedir', '--something') end end context 'repo is not in our own org' do context 'github' do let(:opts) do { 'no_change' => true, 'host_configs' => { 'default' => { 'use_forks' => true, }, 'github.com' => { 'user' => 'myuser', }, }, } end let(:repo) do 'git@github.com:somethingelse/repo.git' end it 'uses forge and sets upstream' do expect(forge_double).to receive(:run).with( 'repo', 'fork', '--clone', repo, 'repo', '--fork-name', 'repo' ) expect(Dir).to receive(:chdir).with('repo').and_yield expect(sj).to receive(:main_branch).and_return('main') expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') sj.smartclone(repo) end it 'passes additional arguments to gh repo fork' do expect(forge_double).to receive(:run).with( 'repo', 'fork', '--clone', repo, 'somedir', '--fork-name', 'repo', '--something' ) expect(Dir).to receive(:chdir).with('somedir').and_yield expect(sj).to receive(:main_branch).and_return('main') expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') sj.smartclone(repo, 'somedir', '--something') end context 'with no_fork set' do let(:opts) do { 'no_change' => true, 'github_user' => 'myuser', 'forge_type' => 'github', 'use_forks' => false, } end it 'bypasses fork' do expect(forge_double).to_not receive(:run) expect(forge_double).to_not receive(:run_nofail) expect(sj).to receive(:git).with('clone', repo, 'repo') sj.smartclone(repo) end end end context 'gitlab' do let(:forge_type) { 'gitlab' } let(:opts) do { 'no_change' => true, 'host_configs' => { 'default' => { 'use_forks' => true, }, 'gitlab.com' => { 'user' => 'myuser', }, }, } end let(:repo) do 'git@gitlab.com:somethingelse/repo.git' end let(:shell_out) do double('shell_out') end it 'uses forge and sets upstream' do expect(forge_double).to receive(:run_nofail).with( 'repo', 'fork', 'somethingelse/repo', '--name', 'repo', '--clone=false' ).and_return(shell_out) expect(shell_out).to receive(:error?).and_return(false) expect(sj).to receive(:git).with('clone', repo, 'repo') expect(Dir).to receive(:chdir).with('repo').exactly(2).times.and_yield expect(sj).to receive(:git).with('remote', 'rename', 'origin', 'upstream') expect(sj).to receive(:git).with( 'remote', 'add', 'origin', 'git@gitlab.com:myuser/repo.git' ) expect(sj).to receive(:main_branch).and_return('main') expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') sj.smartclone(repo) end it 'ignores error 409 from "glab repo fork"' do expect(forge_double).to receive(:run_nofail).with( 'repo', 'fork', 'somethingelse/repo', '--name', 'repo', '--clone=false' ).and_return(shell_out) expect(shell_out).to receive(:error?).and_return(true) expect(shell_out).to receive(:stderr).and_return(' 409 ') expect(sj).to receive(:git).with('clone', repo, 'repo') expect(Dir).to receive(:chdir).with('repo').exactly(2).times.and_yield expect(sj).to receive(:git).with('remote', 'rename', 'origin', 'upstream') expect(sj).to receive(:git).with( 'remote', 'add', 'origin', 'git@gitlab.com:myuser/repo.git' ) expect(sj).to receive(:main_branch).and_return('main') expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') sj.smartclone(repo) end it 'passes additional arguments to git clone' do expect(forge_double).to receive(:run_nofail).with( 'repo', 'fork', 'somethingelse/repo', '--name', 'repo', '--clone=false' ).and_return(shell_out) expect(shell_out).to receive(:error?).and_return(false) expect(sj).to receive(:git).with('clone', repo, 'somedir', '--something') expect(Dir).to receive(:chdir).with('somedir').exactly(2). times.and_yield expect(sj).to receive(:git).with('remote', 'rename', 'origin', 'upstream') expect(sj).to receive(:git).with( 'remote', 'add', 'origin', 'git@gitlab.com:myuser/repo.git' ) expect(sj).to receive(:main_branch).and_return('main') expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') sj.smartclone(repo, 'somedir', '--something') end context 'with no_fork set' do let(:opts) do { 'no_change' => true, 'github_user' => 'myuser', 'forge_type' => 'github', 'use_forks' => false, } end it 'bypasses fork' do expect(forge_double).to_not receive(:run) expect(forge_double).to_not receive(:run_nofail) expect(sj).to receive(:git).with('clone', repo, 'repo') sj.smartclone(repo) end end end end end end sugarjar-4.0.0/spec/commands/up_spec.rb000066400000000000000000000041361523615150300200540ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end context '#rebase' do it 'uses remote tracked branch, if it exists' do expect(sj).to receive(:fetch_upstream) expect(sj).to receive(:current_branch).and_return('foo') expect(sj).to receive(:tracked_branch).with(:fallback => false). and_return('upstream/main') expect(sj).to receive(:git_nofail).with('rebase', 'upstream/main') sj.send(:rebase) end it 'uses local tracked branch, if it exists' do expect(sj).to receive(:fetch_upstream) expect(sj).to receive(:current_branch).and_return('foo') expect(sj).to receive(:tracked_branch).with(:fallback => false). and_return('bar') expect(sj).to receive(:git_nofail).with('rebase', 'bar') sj.send(:rebase) end it 'uses most-main if no tracked branch' do expect(sj).to receive(:fetch_upstream) expect(sj).to receive(:current_branch).and_return('foo') expect(sj).to receive(:tracked_branch).with(:fallback => false). and_return(nil) expect(sj).to receive(:all_remotes).and_return(%w{upstream origin}) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(%w{main foo}) expect(sj).to receive(:git).with('branch', '-u', 'upstream/main') expect(sj).to receive(:git_nofail).with('rebase', 'upstream/main') sj.send(:rebase) end it 'warns about potentially incorrect tracked branches' do expect(sj).to receive(:fetch_upstream) expect(sj).to receive(:current_branch).and_return('foo') expect(sj).to receive(:tracked_branch).with(:fallback => false). and_return('origin/foo') expect(sj).to receive(:all_remotes).and_return(%w{upstream origin}) expect(sj).to receive(:all_local_branches).at_least(1).times. and_return(%w{main foo}) expect(SugarJar::Log).to receive(:warn).with(/rebasing on the wrong/) expect(sj).to receive(:git_nofail).with('rebase', 'origin/foo') sj.send(:rebase) end end end sugarjar-4.0.0/spec/commands_spec.rb000066400000000000000000000316231523615150300174310ustar00rootroot00000000000000require_relative '../lib/sugarjar/commands' require_relative '../lib/sugarjar/util' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end context '#_config_for_host' do let(:base_config) do { 'host_configs' => { 'default' => { 'use_forks' => true, }, 'github.com' => { 'user' => 'ghuser', }, 'gitlab.com' => { 'user' => 'gluser', }, }, } end it 'provides defaults when no overrides specified' do sj = SugarJar::Commands.new(base_config) c = sj.send(:_config_for_host, 'github.com') { 'forge_type' => 'github', 'forge_host' => 'github.com', 'user' => 'ghuser', 'use_forks' => true, }.each do |k, v| expect(c[k]).to eq(v) end end it 'uses overrides when they are available' do config = base_config.dup config['host_configs']['github.com'] = { 'user' => 'special_user', 'use_forks' => false, } sj = SugarJar::Commands.new(config) c = sj.send(:_config_for_host, 'github.com') { 'forge_type' => 'github', 'user' => 'special_user', 'use_forks' => false, }.each do |k, v| expect(c[k]).to eq(v) end end it 'handles partial overrides' do config = base_config.dup config['host_configs']['github.com']['use_forks'] = false sj = SugarJar::Commands.new(config) c = sj.send(:_config_for_host, 'github.com') { 'forge_type' => 'github', 'user' => 'ghuser', 'use_forks' => false, }.each do |k, v| expect(c[k]).to eq(v) end end it 'picks the correct override from many' do config = base_config.dup { 'github.com' => { 'user' => 'one', 'use_forks' => false, }, 'mycompany.github.com' => { 'user' => 'two', 'use_forks' => false, }, 'something.gitlab.com' => { 'user' => 'three', }, 'gitlab.com' => { 'user' => 'four', }, }.each do |host, cfg| config['host_configs'][host] = cfg end # bypass the git repo we're running in or it will taint the @config expect(SugarJar::Git).to receive(:in_repo?).at_least( :once, ).times.and_return(false) sj = SugarJar::Commands.new(config) { 'github.com' => 'one', 'mycompany.github.com' => 'two', 'something.gitlab.com' => 'three', 'gitlab.com' => 'four', }.each do |host, answer| expect(sj.send(:_config_for_host, host)['user']).to eq(answer) end end it 'honors CLI overrides' do # bypass the git repo we're running in or it will taint the @config expect(SugarJar::Git).to receive(:in_repo?).at_least( :once, ).times.and_return(false) # 'forced_type' isn't a real forge, so avoid actually checking for a # CLI for it allow(SugarJar::Util).to receive(:which_nofail).and_return('/bin/true') config = base_config.dup config['_cli_overrides'] = { 'user' => 'forced_user', 'forge_type' => 'forced_type', } sj = SugarJar::Commands.new(config) %w{github.com gitlab.com}.each do |host| expect(sj.send(:_config_for_host, host)['user']).to eq('forced_user') expect(sj.send(:_config_for_host, host)['forge_type']).to eq( 'forced_type', ) end end end context '#set_commit_template' do it 'Does nothing if not in repo' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'commit_template' => '.commit_template.txt' }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:in_repo?).and_return(false) expect(SugarJar::Log).to receive(:debug).with(/Skipping/) sj.send(:set_commit_template) end it 'Errors out of template does not exist' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'commit_template' => '.commit_template.txt' }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:in_repo?).and_return(true) expect(SugarJar::Git).to receive(:repo_root).and_return('/nonexistent') expect(File).to receive(:exist?). with('/nonexistent/.commit_template.txt').and_return(false) expect(SugarJar::Log).to receive(:fatal).with(/exist/) expect { sj.send(:set_commit_template) }.to raise_error(SystemExit) end it 'Does not set the template if it is already set' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'commit_template' => '.commit_template.txt' }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:in_repo?).and_return(true) expect(SugarJar::Git).to receive(:repo_root).and_return('/nonexistent') expect(File).to receive(:exist?). with('/nonexistent/.commit_template.txt').and_return(true) so = double('shell_out') expect(so).to receive(:error?).and_return(false) expect(so).to receive(:stdout).and_return(".commit_template.txt\n") expect(sj).to receive(:git_nofail).and_return(so) expect(SugarJar::Log).to receive(:debug).with(/already/) sj.send(:set_commit_template) end it 'warns (and sets) if overwriting template' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'commit_template' => '.commit_template.txt' }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:in_repo?).and_return(true) expect(SugarJar::Git).to receive(:repo_root).and_return('/nonexistent') expect(File).to receive(:exist?). with('/nonexistent/.commit_template.txt').and_return(true) so = double('shell_out') expect(so).to receive(:error?).and_return(false) expect(so).to receive(:stdout).and_return(".not_commit_template.txt\n") expect(sj).to receive(:git_nofail).and_return(so) expect(sj).to receive(:git).with( 'config', '--local', 'commit.template', '.commit_template.txt' ) expect(SugarJar::Log).to receive(:warn).with(/^Updating/) sj.send(:set_commit_template) end it 'sets the template when none is set' do expect(SugarJar::RepoConfig).to receive(:config).and_return( { 'commit_template' => '.commit_template.txt' }, ) sj = SugarJar::Commands.new({ 'no_change' => true }) expect(SugarJar::Git).to receive(:in_repo?).and_return(true) expect(SugarJar::Git).to receive(:repo_root).and_return('/nonexistent') expect(File).to receive(:exist?). with('/nonexistent/.commit_template.txt').and_return(true) so = double('shell_out') expect(so).to receive(:error?).and_return(true) expect(sj).to receive(:git_nofail).and_return(so) expect(sj).to receive(:git).with( 'config', '--local', 'commit.template', '.commit_template.txt' ) expect(SugarJar::Log).to receive(:debug).with(/^Setting/) sj.send(:set_commit_template) end end context '#fprefix' do it 'Adds prefixes when needed' do sj = SugarJar::Commands.new( { 'no_change' => true, 'host_configs' => { 'default' => { 'feature_prefix' => 'someuser/' }, }, }, ) expect(sj).to receive(:all_local_branches).and_return(['/nonexistent']) expect(sj.send(:fprefix, 'test')).to eq('someuser/test') end it 'Does not add prefixes when not needed' do sj = SugarJar::Commands.new({ 'no_change' => true }) expect(sj.send(:fprefix, 'test')).to eq('test') end end context '#extract_org' do # simple ones [ # ssh 'git@github.com:org/repo.git', # http 'http://github.com/org/repo.git', # https 'https://github.com/org/repo.git', # shortname 'org/repo', ].each do |url| it "extracts the org from #{url}" do expect(sj.send(:extract_org, url)).to eq('org') end end # glab can nest orgs and the whole path of orgs # must be referenced # single-nested orgs [ # ssh 'git@gitlab.com:project/org/repo.git', # http 'http://github.com/project/org/repo.git', # https 'https://github.com/project/org/repo.git', # shortname 'project/org/repo', ].each do |url| it "extracts the org from #{url}" do expect(sj.send(:extract_org, url)).to eq('project/org') end end # double-nested orgs [ # ssh 'git@gitlab.com:some/project/org/repo.git', # http 'http://github.com/some/project/org/repo.git', # https 'https://github.com/some/project/org/repo.git', # shortname 'some/project/org/repo', ].each do |url| it "extracts the org from #{url}" do expect(sj.send(:extract_org, url)).to eq('some/project/org') end end end context '#extract_repo' do [ # ssh 'git@github.com:org/repo.git', # http 'http://github.com/org/repo.git', # https 'https://github.com/org/repo.git', # shortname 'org/repo', ].each do |url| it "extracts the repo from #{url}" do expect(sj.send(:extract_repo, url)).to eq('repo') end end end context '#forked_repo' do [ # ssh 'git@github.com:org/repo.git', # http 'http://github.com/org/repo.git', # https 'https://github.com/org/repo.git', ].each do |url| it "generates correct URL from #{url}" do expect(sj.send(:forked_repo, url, 'test')). to eq('git@github.com:test/repo.git') end end context 'given short names' do # shortname url = 'org/repo' it 'generates correct URL from shortnames on GH' do expect(SugarJar::Git).to receive(:in_repo?).at_least( :once, ).times.and_return(false) sj = SugarJar::Commands.new( { 'no_change' => true, 'default_forge_host' => 'github.com' }, ) expect(sj.send(:forked_repo, url, 'test')). to eq('git@github.com:test/repo.git') end it 'generates correct URL from shortnames on GL' do expect(SugarJar::Git).to receive(:in_repo?).at_least( :once, ).times.and_return(false) sj = SugarJar::Commands.new( { 'no_change' => true, 'default_forge_host' => 'gitlab.com' }, ) expect(sj.send(:forked_repo, url, 'test')). to eq('git@gitlab.com:test/repo.git') end end end context '#canonicalize_repo' do [ # ssh 'git@github.com:org/repo.git', # http 'http://github.com/org/repo.git', # https 'https://github.com/org/repo.git', ].each do |url| it "keeps fully-qualified URL #{url} the same" do expect(sj.send(:canonicalize_repo, url)).to eq(url) end end context 'given short names' do # shortname url = 'org/repo' it "canonicalizes short name #{url} on GH" do expect(SugarJar::Git).to receive(:in_repo?).at_least( :once, ).times.and_return(false) sj = SugarJar::Commands.new( { 'no_change' => true, 'default_forge_host' => 'github.com' }, ) expect(sj.send(:canonicalize_repo, url)). to eq('git@github.com:org/repo.git') end it "canonicalizes short name #{url} on GH" do expect(SugarJar::Git).to receive(:in_repo?).at_least( :once, ).times.and_return(false) sj = SugarJar::Commands.new( { 'no_change' => true, 'default_forge_host' => 'gitlab.com' }, ) expect(sj.send(:canonicalize_repo, url)). to eq('git@gitlab.com:org/repo.git') end end end context '#repo_shortname' do [ # ssh 'git@github.com:org/repo.git', # http 'http://github.com/org/repo.git', # https 'https://github.com/org/repo.git', ].each do |url| it "shortens #{url} properly" do expect(sj.send(:repo_shortname, url)).to eq('org/repo') end end [ # ssh 'git@github.com:project/org/repo.git', # http 'http://github.com/project/org/repo.git', # https 'https://github.com/project/org/repo.git', ].each do |url| it "shortens #{url} properly" do expect(sj.send(:repo_shortname, url)).to eq('project/org/repo') end end [ # ssh 'git@github.com:some/project/org/repo.git', # http 'http://github.com/some/project/org/repo.git', # https 'https://github.com/some/project/org/repo.git', # returns exactly the same thing if no URL 'some/project/org/repo', ].each do |url| it "shortens #{url} properly" do expect(sj.send(:repo_shortname, url)).to eq('some/project/org/repo') end end end end sugarjar-4.0.0/spec/repoconfig_spec.rb000066400000000000000000000133751523615150300177670ustar00rootroot00000000000000require_relative '../lib/sugarjar/repoconfig' describe 'SugarJar::RepoConfig' do context '#config' do it 'properly reads config' do expected = { 'lint' => [ 'somecommand', 'another command', ], 'unit' => [ 'test', ], 'on_push' => [ 'lint', ], } expect(SugarJar::Git).to receive(:in_repo?).and_return(true) expect(SugarJar::RepoConfig).to receive(:repo_config_path). and_return('some_path') expect(SugarJar::RepoConfig).to receive(:config_file?).with('some_path'). and_return(true) expect(YAML).to receive(:safe_load_file).and_return(expected) data = SugarJar::RepoConfig.config('whatever') # we gave it expected, this test basically makes sure we don't # break the data along the way expect(data).to eq(expected) end it 'merges include_from into config' do base = { 'include_from' => 'additional', 'top1' => ['entryA'], 'top2' => { 'top2key1' => 'a', 'top2key2' => 'b', }, } additional = { # array merge 'top1' => ['entryB'], 'top2' => { # key overwrite 'top2key1' => 'new', # additional key 'top2key3' => 'c', }, } expected = { 'top1' => %w{entryA entryB}, 'top2' => { 'top2key1' => 'new', 'top2key2' => 'b', 'top2key3' => 'c', }, } expect(SugarJar::Git).to receive(:in_repo?).at_least(1).times. and_return(true) allow(SugarJar::RepoConfig).to receive(:repo_config_path). with('base').and_return('base') allow(SugarJar::RepoConfig).to receive(:repo_config_path). with('additional').and_return('additional') allow(SugarJar::RepoConfig).to receive(:config_file?). and_return(true) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('base').and_return(base) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('additional').and_return(additional) data = SugarJar::RepoConfig.config('base') expect(data).to eq(expected) end it 'overwrites config with overwrite_from' do base = { 'overwrite_from' => 'additional', 'top1' => ['entryA'], 'top2' => { 'top2key1' => 'a', 'top2key2' => 'b', }, } additional = { 'new' => ['thing'], } expect(SugarJar::Git).to receive(:in_repo?).at_least(1).times. and_return(true) %w{base additional}.each do |word| allow(SugarJar::RepoConfig).to receive(:repo_config_path). with(word).and_return(word) end allow(SugarJar::RepoConfig).to receive(:config_file?). and_return(true) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('base').and_return(base) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('additional').and_return(additional) data = SugarJar::RepoConfig.config('base') # it doesn't matter what's in 'base', we should get 'additional' back expect(data).to eq(additional) end it 'handles recursive includes' do base = { 'include_from' => 'additional', 'top1' => ['entryA'], 'top2' => { 'top2key1' => 'a', 'top2key2' => 'b', }, } additional = { # array merge 'include_from' => 'more', 'top1' => ['entryB'], 'top2' => { # key overwrite 'top2key1' => 'new', # additional key 'top2key3' => 'c', }, } more = { 'other stuff' => { 'things' => 'stuff', }, } expected = { 'top1' => %w{entryA entryB}, 'top2' => { 'top2key1' => 'new', 'top2key2' => 'b', 'top2key3' => 'c', }, 'other stuff' => { 'things' => 'stuff', }, } expect(SugarJar::Git).to receive(:in_repo?).at_least(1).times. and_return(true) %w{base additional more}.each do |word| allow(SugarJar::RepoConfig).to receive(:repo_config_path). with(word).and_return(word) end allow(SugarJar::RepoConfig).to receive(:config_file?). and_return(true) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('base').and_return(base) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('additional').and_return(additional) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('more').and_return(more) data = SugarJar::RepoConfig.config('base') expect(data).to eq(expected) end it "doesn't overwrite from non-existent files" do base = { 'include_from' => 'additional', 'top1' => ['entryA'], 'top2' => { 'top2key1' => 'a', 'top2key2' => 'b', }, } additional = { 'something' => 'else', } expect(SugarJar::Git).to receive(:in_repo?).at_least(1).times. and_return(true) %w{base additional}.each do |word| allow(SugarJar::RepoConfig).to receive(:repo_config_path). with(word).and_return(word) end allow(SugarJar::RepoConfig).to receive(:config_file?). with('base').and_return(true) allow(SugarJar::RepoConfig).to receive(:config_file?). with('additional').and_return(true) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('base').and_return(base) allow(SugarJar::RepoConfig).to receive(:hash_from_file). with('additional').and_return(additional) data = SugarJar::RepoConfig.config('base') expect(data).to eq(data) end end end sugarjar-4.0.0/sugarjar.gemspec000066400000000000000000000023241523615150300165160ustar00rootroot00000000000000require_relative 'lib/sugarjar/version' Gem::Specification.new do |spec| spec.name = 'sugarjar' spec.version = SugarJar::VERSION spec.summary = 'A git/github helper script' spec.authors = ['Phil Dibowitz'] spec.email = ['phil@ipom.com'] spec.license = 'Apache-2.0' spec.homepage = 'https://github.com/jaymzh/sugarjar' spec.required_ruby_version = '>= 3.2' docs = %w{ README.md LICENSE Gemfile sugarjar.gemspec CONTRIBUTING.md CHANGELOG.md } + Dir.glob('examples/*') spec.extra_rdoc_files = docs spec.executables << 'sj' spec.files = Dir.glob('lib/sugarjar/*.rb') + Dir.glob('lib/sugarjar/commands/*.rb') + Dir.glob('bin/*') + Dir.glob('extras/*') spec.add_dependency 'deep_merge' spec.add_dependency 'diffy' spec.add_dependency 'mixlib-log' spec.add_dependency 'mixlib-shellout' spec.add_dependency 'pastel' spec.metadata = { 'rubygems_mfa_required' => 'true', 'bug_tracker_uri' => 'https://github.com/jaymzh/sugarjar/issues', 'changelog_uri' => 'https://github.com/jaymzh/sugarjar/blob/main/CHANGELOG.md', 'homepage_uri' => 'https://github.com/jaymzh/sugarjar', 'source_code_uri' => 'https://github.com/jaymzh/sugarjar', } end