pax_global_header00006660000000000000000000000064152116501170014511gustar00rootroot0000000000000052 comment=37c84efad4a81cea2bfbd5ca381f91ba8ae7b029 sugarjar-3.0.0/000077500000000000000000000000001521165011700133275ustar00rootroot00000000000000sugarjar-3.0.0/.github/000077500000000000000000000000001521165011700146675ustar00rootroot00000000000000sugarjar-3.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001521165011700170525ustar00rootroot00000000000000sugarjar-3.0.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007001521165011700215410ustar00rootroot00000000000000--- 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-3.0.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000012741521165011700226030ustar00rootroot00000000000000--- 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-3.0.0/.github/ISSUE_TEMPLATE/support-request.md000066400000000000000000000005411521165011700225760ustar00rootroot00000000000000--- 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-3.0.0/.github/dependabot.yml000066400000000000000000000003051521165011700175150ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: bundler directory: / schedule: interval: weekly - package-ecosystem: github-actions directory: / schedule: interval: weekly sugarjar-3.0.0/.github/workflows/000077500000000000000000000000001521165011700167245ustar00rootroot00000000000000sugarjar-3.0.0/.github/workflows/check-all-checks.yml000066400000000000000000000007141521165011700225320ustar00rootroot00000000000000name: 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - uses: wechuli/allcheckspassed@204ff63e89eabdc8086f0c50b91f675bcf37c8f6 # v2.4.0 sugarjar-3.0.0/.github/workflows/codeql.yml000066400000000000000000000021131521165011700207130ustar00rootroot00000000000000name: "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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Initialize CodeQL uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: "/language:${{matrix.language}}" sugarjar-3.0.0/.github/workflows/dco.yml000066400000000000000000000013671521165011700202230ustar00rootroot00000000000000name: 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Get PR Commits uses: tim-actions/get-pr-commits@198af03565609bb4ed924d1260247b4881f09e7d # master id: 'get-pr-commits' with: token: ${{ secrets.GITHUB_TOKEN }} - name: DCO Check uses: tim-actions/dco@f2279e6e62d5a7d9115b0cb8e837b777b1b02e21 # master with: commits: ${{ steps.get-pr-commits.outputs.commits }} sugarjar-3.0.0/.github/workflows/dependency-review.yml000066400000000000000000000011141521165011700230610ustar00rootroot00000000000000name: '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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: 'Dependency Review' uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 sugarjar-3.0.0/.github/workflows/lint.yml000066400000000000000000000036411521165011700204210ustar00rootroot00000000000000name: Lint on: push: branches: [ main ] pull_request: branches: [ main ] permissions: {} jobs: rubocop: 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup ruby uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.2' - name: install deps run: bundle install - name: Run rubocop run: bundle exec rubocop --display-cop-names 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: MarkdownLint mdl Action uses: actionshub/markdownlint@2601ac85428122bc70b8d1ec4d539d1cfe17c0b3 # 1.2.0 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Linelint uses: fernandrone/linelint@7907a5dca0c28ea7dd05c6d8d8cacded713aca11 # master id: linelint sugarjar-3.0.0/.github/workflows/unit.yml000066400000000000000000000017461521165011700204360ustar00rootroot00000000000000name: 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Install system dependencies run: |- sudo apt update sudo apt install -y gh glab - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Ruby uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: ${{ matrix.ruby }} - name: Install Ruby dependencies run: bundle install - name: Run rspec run: ./scripts/run_rspec.sh sugarjar-3.0.0/.gitignore000066400000000000000000000023361521165011700153230ustar00rootroot00000000000000*.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-3.0.0/.mdl_style.rb000066400000000000000000000001521521165011700157240ustar00rootroot00000000000000all rule 'MD013', :ignore_code_blocks => true rule 'MD026', :punctuation => '.,:;' exclude_rule 'MD041' sugarjar-3.0.0/.mdlrc000066400000000000000000000000261521165011700144270ustar00rootroot00000000000000style '.mdl_style.rb' sugarjar-3.0.0/.rubocop.yml000066400000000000000000000021501521165011700155770ustar00rootroot00000000000000AllCops: TargetRubyVersion: 3.2 NewCops: enable Exclude: - 'rubygem-sugarjar.spec' Layout/LineLength: Max: 80 Metrics/BlockNesting: 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-3.0.0/.sugarjar.yaml000066400000000000000000000001121521165011700161010ustar00rootroot00000000000000on_push: [lint] lint_list_cmd: scripts/get_linters unit: - scripts/unit sugarjar-3.0.0/CHANGELOG.md000066400000000000000000000146041521165011700151450ustar00rootroot00000000000000# SugarJar Changelog ## 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-3.0.0/CODE_OF_CONDUCT.md000066400000000000000000000064241521165011700161340ustar00rootroot00000000000000# 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-3.0.0/CONTRIBUTING.md000066400000000000000000000015661521165011700155700ustar00rootroot00000000000000# 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-3.0.0/Gemfile000066400000000000000000000001721521165011700146220ustar00rootroot00000000000000source 'https://rubygems.org' gem 'sugarjar', :path => '.' group :test do gem 'mdl' gem 'rspec' gem 'rubocop' end sugarjar-3.0.0/Gemfile.lock000066400000000000000000000042351521165011700155550ustar00rootroot00000000000000PATH remote: . specs: sugarjar (3.0.0) deep_merge 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.6) deep_merge (1.2.2) diff-lcs (1.6.2) ffi (1.17.3) ffi (1.17.3-arm64-darwin) ffi (1.17.3-x86_64-darwin) ffi (1.17.3-x86_64-linux-gnu) json (2.19.8) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) mdl (0.17.0) kramdown (~> 2.5) kramdown-parser-gfm (~> 1.1) mixlib-cli mixlib-config mixlib-shellout 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 (1.28.0) parser (3.3.11.1) 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.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.6) rubocop (1.87.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.49.1) 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) PLATFORMS arm64-darwin ruby x86_64-darwin x86_64-linux DEPENDENCIES mdl rspec rubocop sugarjar! BUNDLED WITH 2.6.4 sugarjar-3.0.0/LICENSE000066400000000000000000000261271521165011700143440ustar00rootroot00000000000000 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-3.0.0/README.md000066400000000000000000000462301521165011700146130ustar00rootroot00000000000000# 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: * [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 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 ` doesn't work. Git will tell you the branch isn't fully merged. You can, of course `git branch -D `, 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 `. 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 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) * Clone your fork * Add the original as an 'upstream' remote Note that it takes short names for repos. No need to specify a full URL, just a $org/$repo. 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. ### Work with stacked branches more 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 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 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! 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 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 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 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`. ### Smartlog 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 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 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. ### And more! 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. In most cases, when using `sj smartclone`, pass in `--forge-host`, and that's about all you need, everything else should be handlded automagically. However, you can set `forge_host` in your global or user config, but since most users will also have a few opensource repos, you can override it in the Repository Config as well. So, for example you might have: ```yaml forge_host: gh.sample.com ``` In your `~/.config/sugarjar/config.yaml`, but if the `.sugarjar.yaml` in your repo has: ```yaml forge_host: github.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-3.0.0/RELEASE_PROCESS.md000066400000000000000000000015261521165011700161330ustar00rootroot00000000000000# Rolling a release ## Optionally, update Gemfile.lock * Update gems with `bundle update --all` * Test to make sure we work with all new deps ## 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='0.0.X' * 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-3.0.0/bin/000077500000000000000000000000001521165011700140775ustar00rootroot00000000000000sugarjar-3.0.0/bin/sj000077500000000000000000000310541521165011700144440ustar00rootroot00000000000000#!/usr/bin/env ruby # SugarJar require 'optparse' require 'mixlib/shellout' require_relative '../lib/sugarjar/commands' require_relative '../lib/sugarjar/config' 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 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('--feature-prefix', 'Prefix to use for feature branches') do |prefix| options['feature_prefix'] = prefix end opts.on( '--forge-host HOST', '--github-host HOST', 'The host of your forge (github, gitlab, etc.). Generally only needed' + ' when cloning (smartclone), as we can usually figure out from within' + ' a cloned repo. Currently accepts --github-host for backwards' + ' compatibility.', ) do |host| options['forge_host'] = host end opts.on('--forge-type TYPE', 'Forge type: github, gitlab') do |type| options['forge_type'] = type end opts.on( '--github-user USER', 'User for github repos, unless specified in the repoconfig.' + ' Defaults to your local username', ) do |user| options['github_user'] = user end opts.on( '--gitlab-user USER', 'User for gitlab repos, unless specified in the repoconfig.' + ' Defaults to your local username', ) do |user| options['gitlab_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( '--[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 # rubocop:disable Layout/HeredocIndentation opts.separator < Create a "feature" branch. It's morally equivalent to "git checkout -b" except it defaults to creating it based on some form of 'master' instead of your current branch. In order of preference it will be upstream/master, origin/master, master, depending upon what remotes are available. Note that you can specify "--feature-prefix" (or add "feature_prefix" to your config) 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. forcepush, fpush 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. forcesync, fsync See 'sync' below, but never tries to rebase, always does a hard reset. globalbranchclean, gbclean [] [] WARNING: EXPERIMENTAL COMMAND. Combination of "lbclean" and "rbclean". Cleans up both local and remote branches safely. See those commands for details. globalbranchcleanall, gbcleanall [] WARNING: EXPERIMENTAL COMMAND. Safely clean all branches, both local and remote. See "gbclean" for details. lint Run any linters configured in .sugarjar.yaml. localbranchclean, lbclean [] If safe, delete the current branch (or the specified branch). Unlike "git branch -d", lbclean can handle squash-merged branches. Think of it as a smarter "git branch -d". Aliased to 'bclean' for backwards compatibility. localbranchcleanall, lbcleanall Walk all branches, and try to delete them if it's safe. See "lbclean" for details. Aliased to 'bcleanall' for backwards compatibility. pullsuggestions, ps 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. remotebranchclean, rbclean [] [] WARNING: EXPERIMENTAL COMMAND. Similar to lbclean, 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 rclean" to clean the remote branch with the same name as the local one. Note that you probably want "sclean", 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. So if main contains everything that 2.0-devel and 3.0-devel has, then those branches will be deleted. Use with caution. remotebranchcleanall, rbcleanall [] WARNING: EXPERIMENTAL COMMAND. Walk all remote branches, and try to delete them if it's safe. See "rbclean" for details. smartclone, sclone A smart wrapper to "git clone" that handles forking and managing remotes for you. It will clone a git repository using hub-style short name ("$org/$repo"). If the org of the repository is not the same as your github-user then it will fork the repo for you to your account (if not already done) and then setup your remotes so that "origin" is your fork and "upstream" is the upstream. smartlog, sl Inspired by Facebook's "sl" extension to Mercurial, this command will show you a tree of all your local branches relative to your upstream. smartpullrequest, smartpr, spr A smart wrapper to "hub pull-request" that checks if your repo is dirty before creating the pull request. smartpush, spush A smart wrapper to "git push" that runs whatever is defined in "on_push" in .sugarjar.yml, and only pushes if they succeed. subfeature, sf An alias for 'sj feature ' sync 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`. unit Run any unitests configured in .sugarjar.yaml. up [] Rebase the current branch (or specified branch) intelligently. In most causes this will check for a main (or master) branch on upstream, then origin. If a branch explicitly tracks something else, then that will be used, instead. upall Same as "up", but for all branches. COMMANDTEXT # rubocop:enable Layout/HeredocIndentation 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' puts parser 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-3.0.0/examples/000077500000000000000000000000001521165011700151455ustar00rootroot00000000000000sugarjar-3.0.0/examples/sample_config.yaml000066400000000000000000000015441521165011700206430ustar00rootroot00000000000000# 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' ] # User to use when cloning new github repos github_user: c00ldude # User to use when cloning new gitlab repos gitlab_user: c00ldude sugarjar-3.0.0/examples/sample_repoconfig.yaml000066400000000000000000000071411521165011700215300ustar00rootroot00000000000000# 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: - scripts/run_rubocop.sh - 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: - bundle exec rspec - 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 # `github_user` is the user to use when talking to GitHub. Overrides any such # setting in the regular SugarJar config. Most useful when in the # `include_from` file. github_user: myuser # `gitlab_user` is the user to use when talking to GitLab. Overrides any such # setting in the regular SugarJar config. Most useful when in the # `include_from` file. gitlab_user: myuser # `forge_host` is the GitHub/GitLab host to use when talking to hosted versions # of these services. forge_host: github.sample.com sugarjar-3.0.0/extras/000077500000000000000000000000001521165011700146355ustar00rootroot00000000000000sugarjar-3.0.0/extras/sugarjar_completion.bash000066400000000000000000000022431521165011700215440ustar00rootroot00000000000000# bash completion for sugarjar SJCONFIG="$HOME/.config/sugarjar/config.yaml" _sugarjar_completions() { if [ "${#COMP_WORDS[@]}" -eq 2 ]; then return fi local -a suggestions # grap the feature_prefix if we have one 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 prefix='' if [ -e "$SJCONFIG" ]; then if type yq &>/dev/null; then prefix=$(yq .feature_prefix $SJCONFIG) else # the xargs removes extra spaces prefix=$(grep feature_prefix $SJCONFIG | cut -f2 -d: | xargs) fi fi case "${COMP_WORDS[1]}" in co|checkout|bclean) local branches=$(git branch | sed -e 's/* //g' | xargs) if [ -n "$prefix" ]; then local branches=$(echo $branches | sed -e "s!$prefix!!g") fi suggestions=($(compgen -W "$branches" -- "${COMP_WORDS[2]}")) COMPREPLY=("${suggestions[@]}") ;; *) return esac } complete -F _sugarjar_completions sj sugarjar-3.0.0/images/000077500000000000000000000000001521165011700145745ustar00rootroot00000000000000sugarjar-3.0.0/images/bclean.png000066400000000000000000000032641521165011700165330ustar00rootroot00000000000000PNG  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-3.0.0/images/bcleanall.png000066400000000000000000000047771521165011700172360ustar00rootroot00000000000000PNG  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-3.0.0/images/feature.png000066400000000000000000000040221521165011700167330ustar00rootroot00000000000000PNG  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-3.0.0/images/smartlog.png000066400000000000000000000111151521165011700171310ustar00rootroot00000000000000PNG  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-3.0.0/images/subfeature-detect-missing-base.png000066400000000000000000000046131521165011700233000ustar00rootroot00000000000000PNG  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-3.0.0/images/subfeature-part2-smartlog.png000066400000000000000000000124601521165011700223260ustar00rootroot00000000000000PNG  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-3.0.0/images/subfeature-part3-rebase.png000066400000000000000000000221331521165011700217360ustar00rootroot00000000000000PNG  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-3.0.0/images/subfeature-part3.png000066400000000000000000000237151521165011700205060ustar00rootroot00000000000000PNG  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-3.0.0/images/subfeature-smartlog.png000066400000000000000000000106241521165011700213000ustar00rootroot00000000000000PNG  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-3.0.0/images/subfeature.png000066400000000000000000000042731521165011700174550ustar00rootroot00000000000000PNG  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-3.0.0/lib/000077500000000000000000000000001521165011700140755ustar00rootroot00000000000000sugarjar-3.0.0/lib/sugarjar/000077500000000000000000000000001521165011700157135ustar00rootroot00000000000000sugarjar-3.0.0/lib/sugarjar/commands.rb000066400000000000000000000275521521165011700200540ustar00rootroot00000000000000require '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/pullsuggestions' require_relative 'commands/push' require_relative 'commands/smartclone' require_relative 'commands/smartpullrequest' require_relative 'commands/up' require_relative 'log' require_relative 'repoconfig' require_relative 'util' 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}") @ignore_dirty = options['ignore_dirty'] @ignore_prerun_failure = options['ignore_prerun_failure'] @repo_config = SugarJar::RepoConfig.config SugarJar::Log.debug("Repoconfig: #{@repo_config}") @color = options['color'] @pr_autofill = options['pr_autofill'] @pr_autostack = options['pr_autostack'] @feature_prefix = options['feature_prefix'] @checks = {} @main_branch = nil @main_remote_branches = {} # This is CONFIGURED host, which may be null, as opposed # to the method forge_host which will always return something @_forge_host = @repo_config['forge_host'] || options['forge_host'] @repo_forge = @repo_config['forge_type'] || options['forge_type'] || _determine_forge_type unless @repo_forge.nil? cmd = _forge_cmd unless SugarJar::Util.which_nofail(cmd) die("No '#{cmd}' found, please install it'") end end user_option = "#{@repo_forge}_user" @forge_user = @repo_config[user_option] || options[user_option] # Tell the cli where to talk to, if not default if @_forge_host ENV['GH_HOST'] = @_forge_host ENV['GL_HOST'] = @_forge_host end return if options['no_change'] set_commit_template if @repo_config['commit_template'] end private def forked_repo(repo, username) repo = extract_repo(repo) "git@#{forge_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@') # otherwise, it's a shortname cr = "git@#{forge_host}:#{repo}.git" SugarJar::Log.debug("canonicalized #{repo} to #{cr}") cr end def forge_host # if one is specifically configured, use that return @_forge_host if @_forge_host # otherwise, if we're in a repo, use the hostname of the remote if SugarJar::Util.in_repo? extract_host(remote_url_map['origin']) else @repo_forge == 'gitlab' ? 'gitlab.com' : 'github.com' end end def repo_shortname(repo) # if it's already a shortname, return return repo unless repo.start_with?('http', 'git@') # otherwise, parse it if repo.start_with?('http') bits = repo.split('/') elsif repo.start_with?('git@') relevant = repo.split(':').last bits = relevant.split('/') end repo = bits[-1].gsub('.git', '') org = bits[-2] "#{org}/#{repo}" end def set_commit_template unless SugarJar::Util.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 "#{Util.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::Util.in_repo? die('sugarjar must be run from inside a git repo') end def dirty_check! return unless dirty? if @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 color(string, *colors) if @color pastel.decorate(string, *colors) else string end end def pastel @pastel ||= begin require 'pastel' Pastel.new end end def forge_cli_avail? !!SugarJar::Util.which_nofail(_forge_cmd) end def fprefix(name) return name unless @feature_prefix return name if name.start_with?(@feature_prefix) return name if all_local_branches.include?(name) newname = "#{@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_name SugarJar::Util.repo_root.split('/').last end def extract_org(repo) if repo.start_with?('http') File.basename(File.dirname(repo)) elsif repo.start_with?('git@') repo.split(':')[1].split('/')[0] else # assume they passed in a ghcli-friendly name repo.split('/').first 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?('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::Util.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::Util.git(*, :color => @color) end def git_nofail(*) SugarJar::Util.git_nofail(*, :color => @color) end def _determine_forge_type return nil unless SugarJar::Util.in_repo? if remote_url_map.values.any? do |x| x.include?('gitlab') end 'gitlab' else 'github' end end def _forge_cmd @repo_forge == 'gitlab' ? 'glab' : 'gh' end def forge(*) if @repo_forge == 'gitlab' SugarJar::Util.glcli(*) else SugarJar::Util.ghcli(*) end end def forge_nofail(*) if @repo_forge == 'gitlab' SugarJar::Util.glcli_nofail(*) else SugarJar::Util.ghcli_nofail(*) end end end end sugarjar-3.0.0/lib/sugarjar/commands/000077500000000000000000000000001521165011700175145ustar00rootroot00000000000000sugarjar-3.0.0/lib/sugarjar/commands/amend.rb000066400000000000000000000006361521165011700211320ustar00rootroot00000000000000require_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-3.0.0/lib/sugarjar/commands/bclean.rb000066400000000000000000000163531521165011700212750ustar00rootroot00000000000000class 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-3.0.0/lib/sugarjar/commands/branch.rb000066400000000000000000000021571521165011700213030ustar00rootroot00000000000000class 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-3.0.0/lib/sugarjar/commands/checks.rb000066400000000000000000000117741521165011700213130ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def lint assert_in_repo! # does not use dirty_check! as we want a custom message if dirty? if @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 s.stdout.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::Util.repo_root Dir.chdir repo_root do checks = get_checks(type) # if we failed to determine the checks, the the checks have effectively # failed return false unless checks checks.each do |check| SugarJar::Log.debug("Running #{type} #{check}") skip_redo = false short = check.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(check).run_command # Linters auto-correct, lets handle that gracefully if type == 'lint' && dirty? SugarJar::Log.info( "[#{type}] #{short}: #{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}] #{short} #{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}] #{short}: #{color('OK', :green)}", ) end end end end end sugarjar-3.0.0/lib/sugarjar/commands/debuginfo.rb000066400000000000000000000005531521165011700220060ustar00rootroot00000000000000require 'json' class SugarJar class Commands def debuginfo(*args) puts "sugarjar version #{SugarJar::VERSION}" puts forge('version').stdout 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-3.0.0/lib/sugarjar/commands/feature.rb000066400000000000000000000036701521165011700215020ustar00rootroot00000000000000class 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-3.0.0/lib/sugarjar/commands/pullsuggestions.rb000066400000000000000000000014711521165011700233130ustar00rootroot00000000000000require_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-3.0.0/lib/sugarjar/commands/push.rb000066400000000000000000000025321521165011700210220ustar00rootroot00000000000000class 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 @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-3.0.0/lib/sugarjar/commands/smartclone.rb000066400000000000000000000046431521165011700222170ustar00rootroot00000000000000class SugarJar class Commands def smartclone(repo, dir = nil, *) reponame = extract_repo(repo) dir ||= reponame org = extract_org(repo) 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 org == @forge_user git('clone', canonicalize_repo(repo), dir, *) else if @repo_forge == 'gitlab' _gitlab_clone(org, repo, dir, *) else forge('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *) end # make the main branch track upstream Dir.chdir dir do git('branch', '-u', "upstream/#{main_branch}") end end SugarJar::Log.info('Remotes "origin" and "upstream" configured.') end alias sclone smartclone def _gitlab_clone(_org, repo, dir, *) # 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. # glab requires a short-name for the fork command... shortname = repo_shortname(repo) # We call fork without --clone since --clone can't clone # to another directory. Also, we must specify =false, or it # will prompt s = forge_nofail('repo', 'fork', shortname, '--clone=false') # 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 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, @forge_user) git('remote', 'add', 'origin', fork_url) end end end end sugarjar-3.0.0/lib/sugarjar/commands/smartpullrequest.rb000066400000000000000000000105711521165011700235010ustar00rootroot00000000000000require_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 @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 args.unshift('--fill-first') else args.unshift('--fill') end 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 @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', base) if %w{Y y}.include?(ans) elsif @pr_autostack args.unshift('--base', 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', base.split('/').last) end end # : is the GH API syntax for: # look for a branch of name , from a fork in owner if @repo_forge == 'github' # On GitHub, the head is the org and the *BRANCH* name to use as # the head branch... args.unshift('--head', "#{push_org}:#{curr}") else # 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}/#{reponame}", '--yes') end bin = SugarJar::Util.which(_forge_cmd) subcmd = _pr_cmd SugarJar::Log.trace( "Running: #{bin} #{subcmd} create #{args.join(' ')}", ) system(bin, subcmd, 'create', *args) end alias spr smartpullrequest alias smartpr smartpullrequest private def _pr_cmd @repo_forge == '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 return if upstream_branch == '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 end end sugarjar-3.0.0/lib/sugarjar/commands/up.rb000066400000000000000000000116451521165011700204740ustar00rootroot00000000000000class 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-3.0.0/lib/sugarjar/config.rb000066400000000000000000000053761521165011700175200ustar00rootroot00000000000000require 'yaml' require_relative 'log' class SugarJar # This parses SugarJar configs (not to be confused with repoconfigs). # This is stuff like log level, github-user, etc. class Config DEFAULTS = { 'github_user' => ENV.fetch('USER'), 'gitlab_user' => ENV.fetch('USER'), 'pr_autofill' => true, 'pr_autostack' => nil, 'color' => true, 'ignore_deprecated_options' => [], }.freeze def self._find_ordered_files [ '/etc/sugarjar/config.yaml', "#{Dir.home}/.config/sugarjar/config.yaml", ].select { |f| File.exist?(f) } 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) warn_on_deprecated_configs(data, f) if data['github_host'] data['forge_host'] = data['github_host'] if data['forge_host'].nil? data.delete('github_host') end # an empty file is a `nil` which you can't merge c.merge!(YAML.safe_load_file(f)) if data SugarJar::Log.debug("Modified config: #{c}") end c end def self.warn_on_deprecated_configs(data, fname) ignore_deprecated_options = data['ignore_deprecated_options'] || [] %w{fallthru gh_cli}.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 # github_host has special handling return unless data['github_host'] if ignore_deprecated_options.include?('github_host') SugarJar::Log.debug( "#{fname}: Deprecated option `github_host` found, but not " + 'warning due to `ignore_deprecated_options` in that file.', ) elsif data.key?('forge_host') SugarJar::Log.warn( "#{fname}: Deprecated option `github_host` found. " + 'Ignoring in favor of newer `force_host` option. You can ' + 'suppress this warning with `ignore_deprecated_options`.', ) else SugarJar::Log.warn( "#{fname}: Deprecated option `github_host` found. " + 'Treating it as if it was `forge_host` for now. Please update ' + 'your config file to use this new option. You can suppress ' + 'this warning with `ignore_deprecated_options`.', ) end end end end sugarjar-3.0.0/lib/sugarjar/log.rb000066400000000000000000000010341521165011700170170ustar00rootroot00000000000000require '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-3.0.0/lib/sugarjar/repoconfig.rb000066400000000000000000000027431521165011700204010ustar00rootroot00000000000000require_relative 'util' 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::Util.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::Util.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-3.0.0/lib/sugarjar/util.rb000066400000000000000000000050701521165011700172170ustar00rootroot00000000000000require '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 def self.git_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([which('git')] + args).run_command end def self.git(*, color: true) s = git_nofail(*, :color => color) s.error! s end def self.ghcli_nofail(*) forge_nofail('gh', *) end def self.ghcli(*) s = ghcli_nofail(*) s.error! s end def self.glcli_nofail(*) forge_nofail('glab', *) end def self.glcli(*) s = glcli_nofail(*) s.error! s end def self.forge_nofail(cli, *args) SugarJar::Log.trace("Running: #{cli} #{args.join(' ')}") bin = which(cli) s = Mixlib::ShellOut.new([bin] + args).run_command if s.error? && s.stderr.include?("#{cli} auth") SugarJar::Log.info( 'glab was run but no gitlab token exists. Will run ' + '"glab auth login" to force\ngh to authenticate...', ) unless system(bin, 'auth', 'login', '-p', 'ssh') SugarJar::Log.fatal( 'That failed, I will bail out. Hub needs to get a github ' + 'token. Try running "gh auth login" (will list info about ' + 'your account) and try this again when that works.', ) exit(1) end end s end def self.in_repo? s = git_nofail('rev-parse', '--is-inside-work-tree') !s.error? && s.stdout.strip == 'true' end def self.repo_root git('rev-parse', '--show-toplevel').stdout.strip end end end sugarjar-3.0.0/lib/sugarjar/version.rb000066400000000000000000000000561521165011700177260ustar00rootroot00000000000000class SugarJar VERSION = '3.0.0'.freeze end sugarjar-3.0.0/packaging/000077500000000000000000000000001521165011700152535ustar00rootroot00000000000000sugarjar-3.0.0/packaging/README-brew.md000066400000000000000000000013011521165011700174620ustar00rootroot00000000000000# Homebrew Packaging Notes ## 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-3.0.0/packaging/README-fedora.md000066400000000000000000000043111521165011700177670ustar00rootroot00000000000000# 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-3.0.0/packaging/Vagrantfile000066400000000000000000000017141521165011700174430ustar00rootroot00000000000000Vagrant.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-3.0.0/packaging/provision.sh000077500000000000000000000010641521165011700176430ustar00rootroot00000000000000#!/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-3.0.0/rubygem-sugarjar.spec000066400000000000000000000045171521165011700175000ustar00rootroot00000000000000# 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-3.0.0/scripts/000077500000000000000000000000001521165011700150165ustar00rootroot00000000000000sugarjar-3.0.0/scripts/get_linters000077500000000000000000000000741521165011700172640ustar00rootroot00000000000000echo "scripts/run_rubocop.sh -D -a scripts/run_mdl.sh -g ." sugarjar-3.0.0/scripts/lint000077500000000000000000000002201521165011700157040ustar00rootroot00000000000000#!/bin/bash scripts/run_rubocop.sh -D "$@" || { echo "Rubocop failed"; exit 1; } scripts/run_mdl.sh "$@" || { echo "Rubocop failed"; exit 1; } sugarjar-3.0.0/scripts/run_mdl.sh000077500000000000000000000004431521165011700170160ustar00rootroot00000000000000#!/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-3.0.0/scripts/run_rspec.sh000077500000000000000000000005011521165011700173510ustar00rootroot00000000000000#!/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-3.0.0/scripts/run_rubocop.sh000077500000000000000000000005141521165011700177120ustar00rootroot00000000000000#!/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-3.0.0/scripts/unit000077500000000000000000000001131521165011700157160ustar00rootroot00000000000000#!/bin/bash scripts/run_rspec.sh "$@" || { echo "rspec failed"; exit 1; } sugarjar-3.0.0/spec/000077500000000000000000000000001521165011700142615ustar00rootroot00000000000000sugarjar-3.0.0/spec/commands/000077500000000000000000000000001521165011700160625ustar00rootroot00000000000000sugarjar-3.0.0/spec/commands/amend_spec.rb000066400000000000000000000031471521165011700205120ustar00rootroot00000000000000require_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-3.0.0/spec/commands/bclean_spec.rb000066400000000000000000000101351521165011700206450ustar00rootroot00000000000000require_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-3.0.0/spec/commands/branch_spec.rb000066400000000000000000000032051521165011700206560ustar00rootroot00000000000000require_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, '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, '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-3.0.0/spec/commands/checks_spec.rb000066400000000000000000000153251521165011700206670ustar00rootroot00000000000000require_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 it 'runs the commands if they exist and returns the results' 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| cmd = "get_#{type}_commands" expect(File).to receive(:exist?).with(cmd).and_return(true) so = double( { :error? => false, :stdout => "#{type}_one\n#{type}_two\n", }, ) expect(Mixlib::ShellOut).to receive(:new).and_return(so) expect(so).to receive(:run_command).and_return(so) expect(sj.get_checks_from_command(type)). to eq(["#{type}_one", "#{type}_two"]) end end end context '#get_checks' do it 'defaults to _command 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 _command 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 _command 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 _command 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::Util).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::Util).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::Util).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 end end sugarjar-3.0.0/spec/commands/feature_spec.rb000066400000000000000000000071011521165011700210530ustar00rootroot00000000000000require_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-3.0.0/spec/commands/smartclone_spec.rb000066400000000000000000000126351521165011700215770ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:opts) do { 'no_change' => true, 'github_user' => 'myuser' } end context '#smartclone' do let(:sj) do SugarJar::Commands.new(opts) end context 'repo is in our own org' do let(:repo) do 'git@github.com:myuser/repo.git' end it 'uses git' do repo = 'git@github.com:myuser/repo.git' expect(sj).to_not receive(:forge) expect(sj).to receive(:git).with('clone', repo, 'repo') sj.smartclone(repo) end it 'passes additional arguments to git' do expect(sj).to_not receive(:forge) 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, 'github_user' => 'myuser', 'forge_type' => 'github', } end let(:repo) do 'git@github.com:somethingelse/repo.git' end it 'uses forge and sets upstream' do expect(sj).to receive(:forge).with( 'repo', 'fork', '--clone', repo, '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(sj).to receive(:forge).with( 'repo', 'fork', '--clone', repo, 'somedir', '--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 end context 'gitlab' do let(:opts) do { 'no_change' => true, 'github_user' => 'myuser', 'forge_type' => 'gitlab', } 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(sj).to receive(:forge_nofail).with( 'repo', 'fork', 'somethingelse/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(:forked_repo).and_return( 'git@gitlab.com:myuser/repo.git', ) 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(sj).to receive(:forge_nofail).with( 'repo', 'fork', 'somethingelse/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(:forked_repo).and_return( 'git@gitlab.com:myuser/repo.git', ) 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(sj).to receive(:forge_nofail).with( 'repo', 'fork', 'somethingelse/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(:forked_repo).and_return( 'git@gitlab.com:myuser/repo.git', ) 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 end end end end sugarjar-3.0.0/spec/commands/up_spec.rb000066400000000000000000000041361521165011700200510ustar00rootroot00000000000000require_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-3.0.0/spec/commands_spec.rb000066400000000000000000000155351521165011700174320ustar00rootroot00000000000000require_relative '../lib/sugarjar/commands' require_relative '../lib/sugarjar/util' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) 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::Util).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::Util).to receive(:in_repo?).and_return(true) expect(SugarJar::Util).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::Util).to receive(:in_repo?).and_return(true) expect(SugarJar::Util).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::Util).to receive(:in_repo?).and_return(true) expect(SugarJar::Util).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::Util).to receive(:in_repo?).and_return(true) expect(SugarJar::Util).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, '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 [ # 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 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(sj).to receive(:forge_host).and_return('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(sj).to receive(:forge_host).and_return('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(sj).to receive(:forge_host).and_return('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(sj).to receive(:forge_host).and_return('gitlab.com') expect(sj.send(:canonicalize_repo, url)). to eq('git@gitlab.com:org/repo.git') end end end end sugarjar-3.0.0/spec/repoconfig_spec.rb000066400000000000000000000134021521165011700177530ustar00rootroot00000000000000require_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::Util).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::Util).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::Util).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::Util).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::Util).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-3.0.0/sugarjar.gemspec000066400000000000000000000022661521165011700165200ustar00rootroot00000000000000require_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 '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