pax_global_header00006660000000000000000000000064150104722630014512gustar00rootroot0000000000000052 comment=f21e2db83f23083b913f933855456d5f712c446a sugarjar-2.0.1/000077500000000000000000000000001501047226300133305ustar00rootroot00000000000000sugarjar-2.0.1/.github/000077500000000000000000000000001501047226300146705ustar00rootroot00000000000000sugarjar-2.0.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001501047226300170535ustar00rootroot00000000000000sugarjar-2.0.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007001501047226300215420ustar00rootroot00000000000000--- 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-2.0.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000012741501047226300226040ustar00rootroot00000000000000--- 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-2.0.1/.github/ISSUE_TEMPLATE/support-request.md000066400000000000000000000005411501047226300225770ustar00rootroot00000000000000--- 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-2.0.1/.github/workflows/000077500000000000000000000000001501047226300167255ustar00rootroot00000000000000sugarjar-2.0.1/.github/workflows/dco.yml000066400000000000000000000006221501047226300202150ustar00rootroot00000000000000name: DCO Check on: [pull_request] jobs: dco_check_job: runs-on: ubuntu-latest name: DCO Check steps: - name: Get PR Commits uses: tim-actions/get-pr-commits@master id: 'get-pr-commits' with: token: ${{ secrets.GITHUB_TOKEN }} - name: DCO Check uses: tim-actions/dco@master with: commits: ${{ steps.get-pr-commits.outputs.commits }} sugarjar-2.0.1/.github/workflows/lint.yml000066400000000000000000000012051501047226300204140ustar00rootroot00000000000000name: Lint on: push: branches: [ main ] pull_request: branches: [ main ] jobs: rubocop: strategy: fail-fast: false runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.2' - name: install deps run: bundle install - name: Run rubocop run: bundle exec rubocop --display-cop-names markdownlint: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: MarkdownLint mdl Action uses: actionshub/markdownlint@1.2.0 sugarjar-2.0.1/.github/workflows/unit.yml000066400000000000000000000010001501047226300204160ustar00rootroot00000000000000name: Unittests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: rspec: strategy: fail-fast: false matrix: ruby: [3.2, 3.3, 3.4] runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Install dependencies run: bundle install - name: Run rspec run: ./scripts/run_rspec.sh sugarjar-2.0.1/.gitignore000066400000000000000000000023361501047226300153240ustar00rootroot00000000000000*.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-2.0.1/.mdl_style.rb000066400000000000000000000001521501047226300157250ustar00rootroot00000000000000all rule 'MD013', :ignore_code_blocks => true rule 'MD026', :punctuation => '.,:;' exclude_rule 'MD041' sugarjar-2.0.1/.mdlrc000066400000000000000000000000261501047226300144300ustar00rootroot00000000000000style '.mdl_style.rb' sugarjar-2.0.1/.rubocop.yml000066400000000000000000000021501501047226300156000ustar00rootroot00000000000000AllCops: 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-2.0.1/.sugarjar.yaml000066400000000000000000000001121501047226300161020ustar00rootroot00000000000000on_push: [lint] lint_list_cmd: scripts/get_linters unit: - scripts/unit sugarjar-2.0.1/CHANGELOG.md000066400000000000000000000121651501047226300151460ustar00rootroot00000000000000# SugarJar Changelog ## 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-2.0.1/CODE_OF_CONDUCT.md000066400000000000000000000064241501047226300161350ustar00rootroot00000000000000# 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-2.0.1/CONTRIBUTING.md000066400000000000000000000015661501047226300155710ustar00rootroot00000000000000# 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-2.0.1/Gemfile000066400000000000000000000001721501047226300146230ustar00rootroot00000000000000source 'https://rubygems.org' gem 'sugarjar', :path => '.' group :test do gem 'mdl' gem 'rspec' gem 'rubocop' end sugarjar-2.0.1/Gemfile.lock000066400000000000000000000043071501047226300155560ustar00rootroot00000000000000PATH remote: . specs: sugarjar (2.0.1) deep_merge mixlib-log mixlib-shellout pastel GEM remote: https://rubygems.org/ specs: ast (2.4.3) chef-utils (18.7.6) concurrent-ruby concurrent-ruby (1.3.5) deep_merge (1.2.2) diff-lcs (1.6.1) ffi (1.17.2) ffi (1.17.2-arm64-darwin) ffi (1.17.2-x86_64-darwin) ffi (1.17.2-x86_64-linux-gnu) json (2.11.3) kramdown (2.5.1) rexml (>= 3.3.9) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) language_server-protocol (3.17.0.4) lint_roller (1.1.0) mdl (0.13.0) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.1) mixlib-cli (~> 2.1, >= 2.1.1) mixlib-config (>= 2.2.1, < 4) 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.3.9) chef-utils parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) racc pastel (0.8.0) tty-color (~> 0.5) prism (1.4.0) racc (1.8.1) rainbow (3.1.1) regexp_parser (2.10.0) rexml (3.4.1) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-core (3.13.3) rspec-support (~> 3.13.0) rspec-expectations (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.3) rubocop (1.75.5) 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.44.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.44.1) parser (>= 3.3.7.2) prism (~> 1.4) ruby-progressbar (1.13.0) tomlrb (2.0.3) tty-color (0.6.0) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) PLATFORMS arm64-darwin ruby x86_64-darwin x86_64-linux DEPENDENCIES mdl rspec rubocop sugarjar! BUNDLED WITH 2.6.4 sugarjar-2.0.1/LICENSE000066400000000000000000000261271501047226300143450ustar00rootroot00000000000000 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-2.0.1/README.md000066400000000000000000000406131501047226300146130ustar00rootroot00000000000000# 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) Welcome to SugarJar - a git/github helper. The only requirements are Ruby, `git`, and [gh](https://cli.github.com/). 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) * [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) * [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 bclean` - it determines if the contents of your branch has been merge and safely deletes if so. ![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) ### 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 ... ``` ### 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) ### Pulling in suggestions from the web When someone 'suggests' a change in the GitHub 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, Ubuntu versions prior to 24.x cannot be updated, so if you're on an older Ubuntu please use [this PPA](https://launchpad.net/~michel-slm/+archive/ubuntu/sugarjar) from our Ubuntu package maintainer. For MacOS users, we recommend using Homebrew - SugarJar is now 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 Like `gh`, SugarJar supports GitHub Enterprise. In fact, we provide extra features just for it. You can set `github_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 github_host: gh.sample.com ``` In your `~/.config/sugarjar/config.yaml`, but if the `.sugarjar.yaml` in your repo has: ```yaml github_host: github.com ``` Then we will configure `gh` to talk to github.com when in that repo. ## 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, 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 manually, 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 anytime soon. 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. So worry not, SugarJar will continue to be maintained and developed. sugarjar-2.0.1/RELEASE_PROCESS.md000066400000000000000000000015261501047226300161340ustar00rootroot00000000000000# 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-2.0.1/bin/000077500000000000000000000000001501047226300141005ustar00rootroot00000000000000sugarjar-2.0.1/bin/sj000077500000000000000000000230541501047226300144460ustar00rootroot00000000000000#!/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( '--github-host HOST', 'The host for "hub". Note that we will set this in the local repo ' + 'config so there is no need to have multiple config files for multiple ' + 'github servers. Put your default one in your config file, and simply ' + 'specify this option the first time you clone or touch a repo and it ' + 'will be part of that repo until changed.', ) do |host| options['github_host'] = host end opts.on('--github-user USER', 'Github username') do |user| options['github_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 <] If safe, delete the current branch (or the specified branch). Unlike "git branch -d", bclean can handle squash-merged branches. Think of it as a smarter "git branch -d". bcleanall Walk all branches, and try to delete them if it's safe. See "bclean" for details. binfo Verbose information about the current branch. br Verbose branch list. An alias for "git branch -v". debuginfo Prints out a bunch of version and config information useful for including in bug reports. feature, f 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. lint Run any linters configured in .sugarjar.yaml. 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. 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 ' 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}") 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}") case subcommand when 'help' puts parser exit when 'debuginfo' extra_opts = [options] end 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-2.0.1/examples/000077500000000000000000000000001501047226300151465ustar00rootroot00000000000000sugarjar-2.0.1/examples/sample_config.yaml000066400000000000000000000013361501047226300206430ustar00rootroot00000000000000# 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' ] sugarjar-2.0.1/examples/sample_repoconfig.yaml000066400000000000000000000057761501047226300215450ustar00rootroot00000000000000# 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 # `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 # `github_host` is the GitHub host to use when talking to GitHub (for hosted # GHE). See `github_user`. github_host: github.sample.com sugarjar-2.0.1/extras/000077500000000000000000000000001501047226300146365ustar00rootroot00000000000000sugarjar-2.0.1/extras/sugarjar_completion.bash000066400000000000000000000022431501047226300215450ustar00rootroot00000000000000# 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-2.0.1/images/000077500000000000000000000000001501047226300145755ustar00rootroot00000000000000sugarjar-2.0.1/images/bclean.png000066400000000000000000000032641501047226300165340ustar00rootroot00000000000000‰PNG  IHDR®+ ÿlEsRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé }ÿ- IDATxÚím²¤* †í®ÞQï ½&ï;ÎX}Þ*kF›1„|ñµmÛ¾‹à `¸€+ ×¾ï þÀßûû4¹D_=¯í´Æµïûöz½ÔB£m›N¬ôï΂g¥éÙ¿HºÒßEõóJå1’Ï©Ò:þF{ýø-½ßq.]/çè[ŒMí¹Kóeuyò’•«ùà©1âŠöþJŠû8´Þ^Öú^¼íœü²–“«sûšÑéé‹F!Y۷Η'ÉÖA×+‹ê'kBÃ=(#ý‰Þì¨ç]A)×äÁ*/-ò•¶_ɘEGêZg³>­©˜R¸ÛŸïuNÕhéK6—Z)ÑïñÈ4¼E±hø{®Våz>÷às©Ÿ³¥Kr©Á«œœN‘U~¬ã[š_VyŽ”“ZêU{_ ¼è\ÁŸ% WŽy9!O=«×—€•¾´ÖP›ˆÒ„mõÈJk#µ~Jtsýl1PVOÒƒÏÒuo#ÑâXYx`é¯Äïö=ŠÊÓÁi™_9Ùò˜§gº~Öø`Ñe-k¤Ñ|~´áš!è]Iß‹—Òo¥ûæ&à(?:uU2*’㙢´(ª+Úמ³7¥æ5¿¼èXœ O>xéLk¦æ‰©ÌÏL‰Rž‘«©*9 fÀýôŒàž/ÝKþg˜G³È¦úøéÅio­PGÔÀ^áíôxÍ)L£ùàM­’KëM¶*DD¿’Q”R©–q²¶÷rþÒtcî>¹6¥ñõ’Á:çþiS…Þòã=ž³Íõe"®Ö" ÍzH-×­¥/ ]®:Ba—þ¥\sÏe]“)µ·ð9å­ÄiýÀ{\JÅVyó-?{Ú{ʶo™×V+¯èIzÆž÷²$~öŒ¯n“-Ø’g Ÿg<Ñ¥z¼ÒOw’‡ÈjÃèuJñõ,$ˆtHŸXð°¬áBI¯42ÃøÖèÌ.#ìl±°áf»Ã¸·áŠ®(x<~°@Äu{ÄÑ —;F-°bÔÀIɲYì¦î§tÓt¿mÛ¾Û¿TÝ7ùmS^OS}V:°öܱÿ¯uÿþk¹žâ|=’~®½õÜz=÷[é>âñûsœÏ·äZî\ÛÞzƒƒƒcÒããÖb³ØŽå×Y#%"+ÀR…l;q?¿…Ô¢Å(}xÀúx{E#£oãä½Y¬+~Šk¿Óñ%úÜ4âb³Ø|dÕ³Y¬õb””+ªH 9~•ÈJC[>e"&¯Íb=þ¾)"#%xBĶjdÕJ |¬€w*´µ¢p}Éjã1KŸ&ÇÌ_ð|Z=«ž/„–v°ðòì¢Ö–F|ŸL[ ’SÖéµëÒýJ…/µs‹ŒYÛ—äÇ·; 4¦|WÜÚpE*ø\%_k1Äê1²ÿ9~¦J<­°Œê£v›­ÖöÞÅ4w”‰Ò¶d¬ˆ°Mv£'LjÉç©X¬t¼^?ÉŸÈÏÖK|]o‘2:(-ðˆˆKJ1¥¿Kï>µx†ç g¡oÙ4·D¿¥Ï¥÷Ç4ý÷ê·"õ2ÖW-¯È$''¹~Y7c.¯uói¢'€á2x‚RºIûroMQ北†¾æ¥cmÿ{¼”‚ÓðÇ«?¥5C­‚´>«¦?Ñí{¯¯´È[N&<äöL—u.ð(ÃuejC¢ßûp$}ë}£ùY2*–5®V'DëøŒj_{ÎÞ”š—¼yÑiq6¸­áŠPô“j†õ°«×YPRírÒízŒûhù`f¼µ“è8"+á®P²=  5úðP:­… Ÿª‘Œ¢”µðÆÚÞ‹oiºQJå¦mJïMy›:çþáÈ€GE\­E­›ÝjßÛѬyoádUî¹>ŒP$š/5Gð¨ÄÍ8ö´÷ègik›1k‡RJ×B'2Š`ºìÈvÚòISU®çÉjc`éodµaôGE=ÖÖ¼ r¤ùëÝ¡À´†ëJ€+ ´‡bŸ©´Ãna¸€™ð†VÂAþ^Ä‘ˆ}IEND®B`‚sugarjar-2.0.1/images/bcleanall.png000066400000000000000000000047771501047226300172370ustar00rootroot00000000000000‰PNG  IHDR¾Tý%R¹sRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé  <:6u UIDATxÚía–¤* F©>½#÷¿„Z“ïÇtõð $Šzï9œ™¢è"òÄøJ)í à!|Ñ€ðŸ¥üÖçš}‹XõŽ»Õo£úßÕúÊÑíà9.5A’ÆÃ§õŸÓ<¾èY¯$¯×ë7ig+Í.óc(ëåQO¯cn³ÚyüœóZûäå{¢5SÍb-?ÚoŸä)ìzyƒQõg¶'È|i/†OŠêGÌòf;ž×†…Ú²îlýË%×^}µåG7zxíÌÔˆêÈnÚÙò^“8Íùª•ií”\ap^?íR§wÿñ>Ÿ6`aot“Šæ¾[ïƒÖ¾t±Ô¶ÐéÁ–õ¬ÕÙºÙ:®ÖñZìD´AißzÞgÄ×ÚgÊ{Ô³€Ó H5<[øz»á Øp/|Ÿµbè%¨3¸y|ÊMWߤX@JÊ]Ò3jÖ Ò-[5 -å,RÊ‚)—ŸS'Ør-€³Dºg#M…¶¥N‹&‘H¤g©ÖFH)—?­A¤µËe4mä©<Á¢@½Ô$JÒ»ùzv¢¥,àòL=ÇW¾\vÖËËóËûu³å  j¯¶9¤ðÙºY©^¾ÕŽTÿ!o`Ñ—ç1!Ë )=(dÁ¢àQÀ£<>€S…oÕb«Ô‹kx| |G ›d®Íw)µgò¤`ѵçé¤ò-Q²©Öäk]ìäß×ì·Ž© ¦=z\,|¥è•ÿÖÄ ö©¼$"–ò½úXâöÞ.Q³#EiÕgæ¸ Xø¬HÁ©#íZ„–™`Ô-;Ñ»#j> FÚŸ(ÉÎÈ›*Vj ßÜâåÝx£îÙ±ßÞ÷ý7•žc-ïà<~#·ôîåY¼-íæ–}Ï|­øy¶ƒgû@€ðÝ…£ƒQœÇížãClàQÀ£<>„ox»:ÀzÂçýèõ;€Ûy|^›OP០գA§ËgÝZv¤X•ÖßU=2ðN)méïRãV|—”ùåR¥Õ,ÃþIûEÙ[yŸÏR¾d§e¯ö·-û½ßø_zÿ¤üs*òjŸµå­ù$‰D:5©buJo$h‹Ž^þ4ýÍfôȬžžÀõ–:GDÆ+Xôil¥Q‹¨mt$€«ðeñòòü^çžØæAž½ìš¼¶wåó»"Žx×ôøj›LJQëåKvJÑÔxˆÒF«ÑK«mJ)7¼;žÆ,ò!ËFÞJËË€kz|K)rã¬}¸´ÇÇ#Ý;g¢ wâŸeÒfèwù½»Ô÷iý˜ë@Ç÷èÌnä[mÃK9@y†4‹˜¥žñèÆgÙ×úVù¼=µùÒïåv,÷^%û±êwo“ÕS¼•Ö9åÅÈ á‹ˆÚc£›V®~!GÖ¿Öž¥äÿ—êáQÇÖ$Ç£ü雞.Ð'ZažHXêè‹ëŒ‹×s`²Úi=+y•ÛÚ~#í­™l­î±E“ÙKñø¤%²òû^0jËÌ4¿`-ö5Ñ—Ïì,8ÿûÖ2ßL°î3b/±?Bô¼<£Z?©Õ˼½u~µAÚWì'·>i€nýd–]^ÐVû’èô.°k¹@HKˆšöñªOëž©å-–cÕÔ'ºüìƲdÚ*?Òßj}Â£ßæv¹Ï`¾#—f$û3¿¥Ù¢yoåèöl‰’åßè$F;q:«|ï8g—½ú›—‘É ÂwàRSÄE¹ÂýÀ£ï31È÷“YoÛ㼟ÝîÌ—ö"ü¤ÈˆG Ò3Ȩ÷ã1hnôðømiÈv×®¥m¬å½ÚMt½V¦õÜœ—XÍØÉëÇDÀàñnRÑÜwëÝÛÐÚ—«^è³(¯²\:s²=8xöîñy·Q«ý5çq¦¼G=[çQÚ°$•·³ÕN¤ p»Õ”…,ÓìjƒãÛäjçÀRßÈÝž‘íæõü ×†&éúõò.à¶Âw‡AàH÷†•M@øá¸_4 |Àõùœíº8N— NIEND®B`‚sugarjar-2.0.1/images/feature.png000066400000000000000000000040221501047226300167340ustar00rootroot00000000000000‰PNG  IHDRÀ,òÔh>sRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé «åõhIDATxÚí]Q’ì*µ§fG½ÿ%dMy¯ro®%ÈA019§ªkº3F ù”RöBA/ÃY@AÐAÄ à¾ï>„Œ§ñgµþÜ…Þ·Íêâiø”Óà¾ïåóù˜'ƒµl­0êûÎ ­3’¾+ë]¥_µò;î±^?þW·wü–®k¿[õ#F«×oM>¯–‡»ÈàU|t ±–^»Å 0Û;n1ø`üñ±z—ôB/ôšãuŒmkÏå{Æk„ËFË{åóM+ç£Þ¨Õ!¢"Æå+yò(Ùf{Wy›Oõ’VPî=þ£ããϺüJF1;r`u*2èä p=Ù¹~QË/…/[ž½7DzžXHý’"j…°´ú½!-=# ŒW‘[ö²Zß‘=1ËD±^Ÿ¡À ¡ñЂË,?ê(!ü×Ê{ä¹5g"æE­T-Š_ãƒUw †Kê¯Ç Cæo¤¬G´«µ…êó(>ÜÞÞ!ÌåfÔÕ¯ŒPZKˆGCUÚ$’&PfèÕ£x2Ë÷ú9Êÿ(yŽªQäQ|è)r¤¿wž¿}«µ‹ÈëèvÀ#C ³CDL]e f9|Ž _¾ü·œ‚YÏÕrhG3úÝ+œƒ~üXâød1;j=J¨²V+3èÔBER?¼B¢NrZŒ«D7ÂG´|”ÓÖã¿TF;Y-#Þ{ÑèÌùÔãóhùhžžÇÝ£=ò­ÇWz^ô×£ ¥ÛÛ¿êíMXë—&SëHþŒÕÚ®Tíz=ZÑôö£ÛÖúe‘“‘òtÖ!ÁmZyÄ@£õD­æ¤>F9×^>·çAøã•m!¢¿-g=ä„î™_YpÉK©„Ÿ¦XOãÉjýÉLØ ÂKÇÈŠ j.g;PÔ9e¹C$«Óÿ|ƒ²'ˆ•”I„ÁY%[ ³ºä;¤4€AñDðmA àOï ùWÌ[wÍËçøÛÞ1^Ì}{íx7€3’þÎ8òÌä¸T~T˜ ŒÍ¶ ïXž±úF4R¾dÁ-ùì—-`<)„ÅÖ«¡VhQ ™¶®GÕã]ÑÒ)=PíéB'Z¿—3ÛÉ‹ÞÏzküÝ„ºFëéÕ/Õg¡'H®"¯GÉOwŒ7oÅÈOt\9Ìn—xöÿçǾßÏŸu¹º¼v¿VFªÇZM§Do&#üñÖ/ÝÒc­ÿÏgëüm•Û”û­×[ÿÓê×~o`»Àxiãq=K~ºüóð¹ŒK1ÊËöÇÚ.?þÀ™`Ð×ôX=׈çS“ã¢õ£ÙÒÃÅ_Å‹wÍ¿ ʳ–úª$ÊCãø,'H{Û½ä¸/ÂßáI:{ÅËsWLŽ;›~7¬ èŠy'ä4äM¨%Ú„ÇF¤ò¼¦#;óLÞ丙i³.§ç{R6ßâ;øM,П0.­ü¢WÈÿ4Dñsdœiü«ÌHÞŒ&Õ®¬ÂVIŽëåf²^hõöïß÷~OJ¯V~µ‘µ*@«Wˆ\&/÷Êù°áá³ÄOt\9Ô±Èv‰G€©Ðˆ´BO_ÒOy ? @â-Jϳ$ý”‚ $‚ø‹‘“ÎÑÃ2¹@W˜”ì?é¥ÜPŸëà×ëYyN«IïûÊxTÖiº»ŸÒ›AŸvб§ôÏX®KíIo»¶üFd-¯É3·'@S¾g”¸¥ÌTÄ­“mÞ7sùÙ|kom êDzhGiA˯þFïYN¤æÄ,¤½ðŠ‡ÛŸ°ºšiŒ"V++ðˆg]Q~¯À·Ì9V;:4~Ä­ à9ŽoM¢ìû£I5Z{×]I‚þHôK^/J'R·ÈdÞ3r-3Œ_Vyˆ–ÿÙóˆ ®œ T [µ2]x’”=£W¿´×£…¤¤ï^ÅlÝãiµ‹†Î¬ÙD,ãAÕ±‘BнôaQ¼×d#ºüÈ* å¿VÞ#ÿ-Y‰˜Gçz¹A,ag†H¤úGÚ²¥ž9µ=®ˆÈh˜.kåÖjÙŒZ¢†=»|¯Ÿ£¡Â(ùªÇã´Äí `FÈ'c2\=Á¬ ûÉŠ€ÊÎ/·£«ïˆ¤é·N¾NNüX…ÿød)°Yâ#Y}e½<»â•RýQÇØ#ó»ZŒ«*Gøˆ–rò¤Ü´½2Ú^\äþéȽ K­½ÏûYöåz{ Öú%%žàÒV/)rKÉ·’v{ÆÅÒ§h«…GåaÄ£ò9R>‚NK‚횯HÒt-TÔ“¹ª%ˆ0}PN©Ð,ï.# |ÐeE>Z=e¼¼ã”YwÔxyë¥7ê0P„SN]8{Ò¾ÁP®LßåôVÔA逌åàLíú½å³&5ÖmM«¿]Oº¶lŸ·Ú±§ž–kZž¿¯p¿D—_9éÜõøÕJv$I6®([åXž–œÊó Üòœ!ã=ÍR[îõgLÑúeŸžò=m-MzüÍ;‹—úå ȵñÑ&;Ö@*½†”iÇÕÊGO¼’Æ_³©×köi÷ÒY‘]WÊañö¸ˆ¢G·=9Ϭ;ªÙ|ª‰r·­xöd k{ W¢x2|{€5QJßÎÀ76ìuÌc/N¸¥&‚ù-$Æøk"’ ´¢lw9(ÁÆà8:•yZÅ–­âºQâÏç߈$#’ 0,Q¶ŠîZÅu#E€IF$`ÊŠRb¤°óÈvzëF$€D †ÄH2À¾¼µ@9;ÉH¯«¬œ\J¿#’ìõ&1Àr+Ê¢Ù^q]ÏŠ‘äuE’="ù-õ¤.ÜŽE%켇U`ýÕ¶çKy±7(+;ïŠà‰`6ˆ¢Xåõ„é®'ïê6´Óv]Æm'Xõ>â41@çŠREâMºÚë¿°žÔ_é>ÂO6O”ð=iÈ,Ïô#ü`Ãûøøÿ0WÜ[¯•/Œ(ÑoËŠ&Z]*)ÞÚþž±³jÕ¶Ú½E ßë‡ãÜ:þÒ+/V¿ªiü¶ŽÉÀϧõ†½§šXw¶è·7àk¿K[f³DÂ=í!=ç±—–À¼bø^Ñû¨‰wü¥¤èõ+K[kíDŠ 8QjÉ,é5J¢âY?»þèñYmeÐb/iüWµó¶¿vð[’ À€D)ÝèÙYªŸ¯lÚKœ<Ê^¥’ÂÚ'€_â][dÍêkõ÷ŠŠg­˜fû_åº×qžcGù´¥ý+cmñ«–¶Xÿ€†e-iÖNñiå£D¿[‘§I”[ZÑdŠxkâäÞINÔuµ-G«Ý½öªCï¸gû¡×¯zí^{~ží·?µ[s agrˆ´Tͯ°'‰2<¨D­`H”€_(–NJ_7 Þ÷ ²³ÖëSƒüSÚÀ >­3ë–-8¯âMoP\%h 3–ÏÏkX°¼¡õ×[^³ËˆwµU_MÜ 5QF&–Ú‰XKB•=úzý{Ò”d¢Ûî…ð–kRc¹ŽÇß®ïU2€i+ÊÖC9–ç†-×ÖzJŸGRíՄ̃)Z¿,ãÓS¾§­5Q}ïµø*6ÈæKÂN; óñØ"ó4l¦OhuG$n$ì kEI°X|VÓ°ÒZ¡îýHYQÀ7o†€D @¢ Q(H”ËóÆ÷­c篎IEND®B`‚sugarjar-2.0.1/images/smartlog.png000066400000000000000000000111151501047226300171320ustar00rootroot00000000000000‰PNG  IHDRl“³¥„iCCPICC profile(‘}‘=HÃ@†ß¦–ŠTDì â¡:YqÔ*¡B¨Zu0¹ôš4$).Ž‚kÁÁŸÅªƒ‹³®®‚ øâäè¤è"%~—ZÄxpwï}ïËÝw€Ð¨0Íê4Ý6ÓÉ„˜Í­ŠáWDЭ!™YÆœ$¥à;¾îàû]œgù×ý9zռŀ€H<Ë Ó&Þ žÞ´ ÎûÄQV’Uâsâ1“.HüÈuÅã7ÎE—ž53éyâ(±Xì`¥ƒYÉÔˆ§ˆcª¦S¾õXå¼ÅY«ÔXëžü…‘¼¾²ÌušÃHbK BA eT`#N»NŠ…4'|üC®_"—B®29P…ÙõƒÿÁïÞZ…É /)’B/Žó1„wfÝq¾§yŸ+½í¯6€™OÒëm-vôm×mMÙ.w€Á'C6eW Ò àýŒ¾) Ü=k^ßZç8}2Ô«Ô ppŒ){ÝçÝÝ}û·¦Õ¿éérpSac« pHYs°°é~œ½tIMEä  AUÊtEXtCommentCreated with GIMPW7IDATxÚík’㬆qWv4û_B¯‰óÃ_sBÂ×÷©©©´ƒ¹Å–„±¥9çmÛè“sÞ? £”Ý…ŽÂsˆ¶Ó¥;϶¥G?ž,"•2‹J·I‘7/+}9,Õ´‹ŽTóxÚ)¥ŸpÍÑ8ãvÏïŽ,z^+˜ö†ûD³õ¸J‘ J¦,’Mî¹Ç¡ÆŠ­Þ®#/Òi€7ðј–µ”Ù¯—+ÍŸ[uyÃ|¨ðmfüÙ|LVsSº)Ÿ^ÿ4*'j•›Ï°íX#ð8ê?©‚~;¼×”O/M}íÒ$6µqØKÍW²ÿ'ª†Ã[¯Âæ ÷öX-÷FP:òÙ µíL¯Ìûs½!xœ|5 là‘|ŽaLŠ¡XõSׇ^qç¼h»“¯†Qí¼nP,îyñá›áÔ87”B™Þ[®Ó+JÅöOI±ÁeGM©F¯¸^ •½tÌl6àQ#ýÔ.ö¬·ó¥½|¨Ìj뤑^çãι—¸ÐÙ`‡‘Nk¨ÑÊl»†ÊðF[õa6cø®.M†ìÈ#d'ðS&À-%&úG£VÃ6~ð>~ÑøsÈ.á ·¾ü¿¿ÿý3]s*MØ?ÓÔêÎJ×óÀä”rDkJåc‡˜ûí`ùÃG÷²®P”W^™R¬ìfõÁÍuÀ•@àndöö—(è ÈûêEñÖ‚'ŽýÓ¿æ÷lopÚ›9ó:ÍçÍ–Ò‘&ý‰~Ó£ðïŸöaÀ"T®À§˜eõº Êë]Äî.‚Yƒ½NÓ\¡‚ã®lÛ〞DËߢ—½¢Çå¦L>Ôi6î®:}sË6÷AàÙæ‘†ü«Ç©¦@xšÝû„P†ów¥N°EkÛ°)€f/«`âɦw}½è†æŠ`'–Äz[²kã³WÜï ͳd%è€FñlíGû5}-H䃅µ»¸s«"f7c»žÌ‚î¸Z°¼$æ)e…#€aÀ8*s›‡¾D¨ùVs׌‘×—ÇV“ËJsáØ¢B¶õÚEë?[ºCñÒÕN>¯— {b”Œ!íô­`Gµ ߴ·jËQg}¨>¹¬À+ç0?ˆ_¾ç÷—? ³Ö9€æ[º:¾vùöÖγ z¹ óɤô¤+=qîè^>š}™KÜk¦Ð:瞃÷uÇðÈ„ÚÑL÷.ôþ—ßMMzZ.[:[7¶ MöÏaK­eÍÔYSºæó°-½Ä@©£|~yåиç÷Ÿw+X–•¹áÅÐêíH²1çMçžÚ:!"„Ò]ÿîîðyÌ£¤†>~Qí^fE“5­óYË6P¿]\îyM+-½X„F|Ÿ8Ó»Eñ«aÁ[ÕL^n+û$qž€©StG´à¾>½€zgË3©CÖµÕkÅ¥Zú›ÝübçáQÕ2_«¨ÀjáPOæ¶¡}¥¾œÄ…‡;týûµ»#0XÞÒ¡À?Tåv—'§¯½q ¨ÓI/xnKƒGrè·FÄïØÐo¬Ô wn`5M}4¥×˳˜5d[✡m¯>´,›Ú~|wŒ¼"µIh Ÿ74Z©¹*[úœ•ò‘.Ù,>_@½uufó¤¡ 5í ìg°jhrp`µuöqošó‚}þ°îÀN`pEϺÀj¢×pk~×Ü—tÞÒ‡ùxÍæ?¼Q@úÀƒ À·Éó0¸*ˆÆŽ_ ÐðÙþ7¥áñMòÁ•ÍÛKö·’ÌcóQžæ;NRs £þÐGÓñŽs«Žfe‡G'šO?å¨Õr D ¸ñ¥x¦›s,Mæ›ðíþ•Òú£‰ÝåÊ".G¤±¦ô¥?G¬©¢ü{ÕÏžã¼VåWQ:€‡(€ý™Þq‹øáWCaÝ)| —‚CT¾YÈ¿Q|Lt9ÄYãªGñÍ ^ãºqønõ^$rŠx"çoÜ]éû4ùú–MôöÈe šÆwEÛ;—®¢$j›#¼ÙdåhòD|5Ãã¹Ëóoº‹Mã»2l»²†‹êž<Ú5¿¿Œ…¾_d-w3§÷mÉÖäÿ¡Ç»×G‚÷gôræ“IéIW:kv÷òqd*šFš+lÍWš±E=–ËÒ|ži…ÐÃF]/h4?k­—‹e qËhÒМÝlS6h׊eG±€5/SÿÜ*jÍyЭ⟌&My„ц&çæJm¶›rÖÔP“xX¶†à] „¡°®z=twéåa6®ä™QÃ4‚·j¯ç6ëV¹còW#U}rÍ$g…úÔ>œã=-´¬^}NYXV HvàôÒ—Õ¢TÄÏOáÒ¥¨ññ'©6×] åñ l#«zè4;½ŠM_–'hV…:Ìóuƒ ê*†¶2g}ÛMCZ¹†#€/i»¿Ãš+åbó§,¾Ù|L¶|úžËíI­»‹¦™[ôr¦i¶ÊŒÎb>ìz}›x=¥Š¢¡J§"ÙG%Tí¾ ‚¬qq˜Loö®"1{9Óúiäú «ªÉÙw€{0ió:'T×׬᛭QXâäÉÈ7—þ‚h;Àkx^ˆ1M;Æ@†‰ Œ9œR6MîûeƒÊÑÛ—ƒ‹“A)¥$Ï™éÓÔÉbƒ¦&‘ç÷)§"•­.Éè¼ñÕz€•Feœe*ìû•Ù^T8ýᡱœsòYuŽ÷[äõiLƒ€Ø}›!†°IP™Fß.vÁ¥r…%v½‚7¿€®õ<ÝUbþ! —Å®ßÍGöFSš†#;yÞf×D:Ò(k¥7ÿ‡÷bÙ;x*ë;"ô®›”˜-Á3½‘ãT첂xв£¦—á¥`7*‡Õ?º&Ÿ 6¼Yž_W(•—,^ÑÞ¶¯Úá¬8ÁFÊÍäy8§§³¢7èiql½Äu‹·Œ{\:c©P†r>úR¦ö̇~Óïré»z[æ ûíæÿ‰½‘¿s¾Ñº¢{<Àö e5”iRšB~L÷£0 ðò½*Ð[ãÈäCâŽ[§ž‡ÜqS.÷ý•̉NM+.Þ§ýêéz…kâÈ€ƒÃ*%öˆ…2 áC•F¯M6ô[Ö­vò4áÞXïÞÆ¿c 7@ÂdQ&âUØþ®lßï~OœmEOéƒí[ªR1š9Ù¢lÅ-z#÷eõ¶TœÚõ 8¨ñ}Ój6“Q7s2”¡f¥VDA'‡©øîïÕóüLúy.¾§[Ìz«¥Ø¼«Äêî`­iÇá­8¦7z¶¸ÜÒUÂS»þW»’Ò\(Ã(¥õ“ êÓÄ~¨S ª¢±ç^ mùIÔxÏ•.ï­ú÷ŒÞ[ºDú_©ë· 9 vY›4ß0n•CL)Mè·¥3HÏôÞuÙ%†³‰Ø ™$3½ú7í ßÚÙÒ5]¯€ƒ¦qIóíŽì5J— eèst]@Ï+m`g†9kîêÉåFà¿ õ\Ò 'SgÚ Ä­xr†i„9¡áñ÷æ‘Lظ»h·M½Í´¬“NJ_ùÅ{Cã_éU¦iûø?¸ë½’k(L™f§LVmsW3ëÛó&)ä³’½¾8LãnWcþ»óy ô¦IÙkƒßå÷½uo\mçíKvÏØÚweøFÕñæ@oùå¿=z$Ïý--„§ÌóSK™ŒòÒÊ=åÏîgÀ-@-ËUÊ,*Ý&E^ˆ#Ìj2;fá•éÙN†>œ¬B4˜B³ì÷µB°¬€ÜLDù£ùµÝ3<Ç,R®×‡Á³8’ƧËZ…Nà …àPòî _p(¡&ÖCÆ5ý“Ènï¥ê ÂìŸÔGaFÝü?̧—ƒ¦>Žvi›Ú8ì¥æ+Ùÿ“ x\óØ\@áÞ«åÞˆ?G>û ¡7Ð+3JÔ‘CT q0ñ9¬¤ùݱê§ÙÀUÃ7o*€cøßö6r¿Õ¸IEND®B`‚sugarjar-2.0.1/images/subfeature-detect-missing-base.png000066400000000000000000000046131501047226300233010ustar00rootroot00000000000000‰PNG  IHDRÌ;ö;º“sRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé *¹HÆáIDATxÚíiŽ#! F«£¾Qî„:SæÏd¦„Xlø µ¼'EÓ]C0³Åõ³mÛg€*/T€ÃÐ;ÌÏçóïeÐítçò>;÷ÀÊÏv8Ãü|>ÛÏÏÙȬiÓŽ˜þÝÑP½2•åSÈÊwU}¯^^k>é`ùýëóïÿ¥ù}/=¯ýž“ïqr­z×úÝÕì)ÊVž¨0®0£g©9Ãûä÷cå1¼ÎŠ¢GþŠöÍÙá×fsöyLßrv#e± ØÞô½ýîIöû•«Z}2fá0«8zpcwã­‘ž¯à Ô»=vš¦¿’ÞI²NBÎ8f–_ﬨ´š›a÷nÙ Ö#¿ÔÁs[O5ùª™c)ß’>½åTÔ÷8‹îÉ7M_j¿V›µÚ×[žÕ¸jr5ÃY*WN-{¨µWO?õÚ§¢¿{ôà[ýç{r‡Y¸j?÷ÌNs†á‘_:juÜRï ,ùzϸr?«ê[Ü­ùÖôæ•_³Ey”žÇQËê=WN?2{õïí­~Úr&½ý=ìõ::åŠ]eçì0Wm1¤§^ªä{ó)çL9¥ôµzy:²ª<3V†%Çnpg Ñé[õݺTõS•Ï$G©¸©ÃŒp0†¶ÊxW9„;žÏl3fï}ö0ººW\œRÉY¥87/«~?Q†0kV6ÒFV éVsKŸªŽ®Zå®Jom“ÒMÆÞ 0ª›®g\:bðèÑ›^5yM·uKm`m¯•¶Ÿ󬺰èa´ü3Æa¬0{·-çl=ÚV9¹çjCëÉ·´å×s&¦¨¯ç2DI~mûÊ{ÙÂ{¹¢¨]ZмˆQkG‹ý¤W”3móVÙjé=Ý+GµZ,ÕÑëܬzƒE»)[¸€íÝì[ý·ÐÖ§â"×ì*òöl¤ÞT—´”íé”TådÜ8¡Ã¤¡Îílaþ€çm¯È¯ˆDÔw´®Øö&ð¶ŸÃ<ÞÀJoˆ)8c¼R@Ÿ€}úu;Ìã×:øâ-FÍè>öÐ 4}Þ·^«Êz×þË–,tApÚ‘z¿^ŠpŸèÿ?¿%¦ŠðÞv .nÍw4ȱ·^^9ª ⪠é–|Kß§ Ž®zoª*È·2檢]jAý­Áõ£Ûñ®öiý^näxhí^=´úb-Š’:ßK;LUÐëž î9…Ϧ®–³2¨¹GŸ¥rDG·×ï òÝ.ª~ä}ÞŠ"d –¡lÇ»Úg©^Ê ô£/èy‰EŸiÝ•ùÞÊaF‡o+EÁ±È  ^2þH9#õ©Ïž(%‘[QÑÁ­ÏdçµúFŸ­z©Â•ís†~fع7ÆÓ¶iOû¶’3Qž-çlú<[ òU³ÓUçµµWZÝ;Øç“X9>¯ä¥T`dã¬ÈW)ÿLAÍ=ú $m ¶í-³¥Lª|[ú‰’o­¯êyt;ÞÕ>W; eÿR¼GöîüŽ6”gÖÛ\<" qm_^ñbàÑ`ê‘AÓ½úLë],¼ä[Õ.#+KðrËóš=´.‘Dµã]í3W¯ý½T¯Rù½zðì‚ÔÚH™ïÙ 4Lߺ™õ·è|Ÿ´¸ª©×3Û‡ všŠÕØUò¥©åÇa<"ý 9Ì}Q‰ö‹ið"å}“X°è€æ“×~Â2ÁGFk`K6š7*8#±/¿¦ÕОq{Å1”Ò¿ÿ÷È7'gOþõæ›¦ß ¿aM~M^«<Á8*˜÷èsïêÉ|Z%‡ ÖÐËg۶϶ÿý7ý]ù|”“ûe¾^ùµßwg¾™Ï—ãïÇ­Ïsr{äXå—þÿ('W/µÅï=òKúRåˇŸ¸-Ò÷nmOªä¼;êµ;òQ•g`»NµÒ;Ê»zðo‚X@Ü–ìÉBøùßMϽƒµõ]‘ ØÛ; b pEü—~öäç½±SÝÝ/žÞâ¼úÈ=iGƒ_q’Ak°­0ß…:}þn¬Ðjé7Cú^9éßxó}'Ïß™‰€GþÑ©îŽò:äZPçV0oKÐkëó‘rªêÛrL±WÿÝzCãyÎøÀ¯Oô;‚X€}… kéYQƒnæ(Zñ+L(P:Ž`âu/^i£«ß ÿ¤Î´ÓÊû4ûfÜ+¿½3¢ž3o„•ÑN1»kÉ]uöuõßF‹ªcuǯÔXž—ò;Fæ©‘Z¾^Ò²;o_.õ;Þî@­M9ˆªÃŒs7){/[`È×q^½“«Ù훳ÃÔyV.h•Ųêó¦¿Ó%§ÈÉqm¢Ï#,øúŒÕ«¸ûÏúU«Ì+ø=‘ƒFƒË_Ao‘cA:1¢ßƒi…YÚ’JÿßÜÚ“×hðìÒó‘ ßVµò­­j<åTÔ·4‹öw¯mEZÚl$èøÙn·*·4g8KÕJÌbµöêé§^ûdµ¡³4pÕ~í•_:juÜRï ,ùzϸr?«ê[Ü­ùÖôæ•_³Ey¼¿w`¶èÁ{®~Äxõïí­~š³EO'{8X0;Ì™[!%ù+Àèø¨ª—¬ÔGäÔÎè+œï¤T­ =g˜½“FëDuUúV=G·.UýT%§g’8Ì©[;3ß*1cknE9ïx~<³Íûìatu¯BO0{ˆäe5Âï'j ™u°>ÒFV éVsKŸªŽ®Zå®Jom“Ò÷èz/À(ÊYºHc½=îÑ£7½jòj ÒŸKSûÞã*ÛÏy‘y¸é ³wKÐrÎÖ:ƒ°Ê/uÖèg=ù–¶üzÎÄõõ\†h_Ï9ïe ï@¹¢¨]ZòÚíˆ]yûÑHzE9-üS½–Ò{ºWNäªn¸›²BãYn»vë ´m¡¸Èu»Š¼=©7Õ%-e;«.?)p!‡Éng q\¯½Îô&þòB8L&àDül“Àùü”\ IEND®B`‚sugarjar-2.0.1/images/subfeature-part2-smartlog.png000066400000000000000000000124601501047226300223270ustar00rootroot00000000000000‰PNG  IHDR”áåA»sRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé  fXC#†IDATxÚík–œ*€5';šý/¡×Äý‘˜á2UP…¢~_Ž'=6ÍK¥ µoÛ–6x%¿èx»"Rúw€ÎÛûçní_¥¾o»oOîÁ¾ekRJÛ¾ïæ‡Üš¶Ëßå…7ÏÈúEä;«Ü+Ú[».ÖòJ!püÆzþø®,ïø[;_û[Êß#¼[íŽè·§)+÷ƒ6.¼…ßg>üÇÀ]Kç0<¸ç Ô#ý-åS ñü³VNÄ5¯)¡é#ûmÅû"RðF àÑzJãÀ›˜¶F`öà÷«û®ÜaðŒöÞô\‹2¼ò5žyM…ðŽcÀ«<š ·ü^r¿öNä†'m€•\µµü{-k¹5kÙSψöjÖÙŠƒ«4p–Rw…X»ŽR½´)‰žû¿u¿ÍxŽ<ýÐ3Ö̘‚x…" Ò Tjú^«§P¼ùksÁ­S`{„¥\ï\·ô9ª½á:*½¹gÀÎëï]·0;ýˆÀñN1xï·Öý/=cÏQž¯¥?[ý`kž4ep‰"p•K®|à£-ö¨ü½åŽÔóÌ|¢Ìšö¬èU2­ŠíUé[íu¡GÝÿQùx”·¨~ˆìO‰ÌÒÒ¯zèG瓯noäõˆtë¿…QoUÍcuU>WõCd>Oã—u8ŽYÐYZúÈ6b–S­þŒh{½3¦ ¢Êi ð¬o©xúË›>J¹*§¤r¤4µ÷ö¯º§¤±ÄÚ–~ðÖ™·:<½®iËQV¹ÖF¯"꽎oâdžB£ÜˆtÕ¾­ý«×wæÛ3û-j1\ÄÚÚx2£½ŒQŠÀSBÞroEÞ»ì´ÇŽ€')ð\ˆ>¸mÛçCpýP¶mó¯Öaéüçó}X~_žr<õÿúú_ü9Ê¿µÃó»+„\Ú®ñùÌ(7Ç_¾¾ž« hoÕ¢ø,ÞÅF^ᜠܸ ün™^ê“RØ÷?Çqþø||çàrµ¾zžô;úºrîPn4=<²1zGL€ïÖŸšpš!´²êŸ»ë<@·GÀºõ§&@{k>€[~_óD½õÏÏÕÚ%Õs ì§7€ =çÁÃAkè_x!Ä@Ñá:<…SŸmÛ<Ó¹½ÛK› •ßµòjí6èÙÈÈ:ÝL׳9OW•;«¾AíYe}¯BÀªt¿5àÝÚUÛ)°¤-Vœ´Ë –Om§C8I‰¸›’²¸ÐÊå¶m`j 7HѨõf R4KØ£DœHP¥e"Q<Õ#à RÔ#xó=ZX‡ ˆVP¡\¸öÑÒ”k :€ÈR_é·{£nZY{ß=+ºå}žŸ¯¥Ïÿ/Ÿ O>R¹µüµýzê (&@íï–e&¹òk.|Ol‚¯A·R`ªÇß=Á‹,éGÊŽ´”„­QŸÚK­–~ØÇïé\±•>÷Gòä£}Öò×ò‰ â(f…Àj©{ÝìÞGÚúSv%¬Yç-‹Õ›>ªÜ-¨ž)(ÿ¨ôž{¼÷Y±”k©ƒ”OT'@˜Ê£çÞ57¸¢wvСÙíº¢Ó¸7@²œOkÂdEƒ NPã6û–¼¤H³ô#=¡AŠ\ÙS„ðìàHiB}>Üc”ÌH%À›WTúr @é˜ÙfÀ#`¢­ÁyV¢ðàEµùöVž½bÙ¶„‘r-Vµ78Ò^x9òÏžüKÅ&9úÁÚ^Åjö/òGêÉÇZ×Z>Qõ€çB¬NïD÷TÆËÞ©ŸNåM ^÷@X_pA\Ÿnô-Š˜!èPoß©ŽúžÃiA‡ZÁ…¤ù\ÏD­ô‘Bpéyç—ó€ÑñïøkGú³ÜøÇÿÚñù´¿ËÓ”é¥ï¬é#YùvÇ¿Ö9Ï÷ú¡uoœÇ÷qyÐ!ËvÀ=é :TñÌþrzС|c ³Ð¦ B¦´€7ż 9U¼A‡J‹ºe]{7 Ò»ã`4ö[9Ò¼ ˜Arªx€‹»GPkéÏØ~ØßqN%âÆÁ|ZÞIq8>#øW@œ6=áA0¢D**Á|z”H‚äœÇiûÔõIA„jBÝ“ÞZ‹wÀµ@NÚs¶²±x0Í Ÿ'HÀ5vß÷¿ùöè£ûxÒG.üQne¯ûªe_[¬WS&JÅ¢5ŸßÊ?uÔ§C],XËèPZuMAX‚ù @0Ýn¦<¥þ€":@ø¦µ(ž]e¬Š€ÈÌW `qE^ªà x©"€ðr¼PÀðbEž [ ¼<(€"&>ÚÏàÇï[)¥mßmêsóe,äóù™VÊGf__òï£ës*)»rÁÊ€¥])ýI‰õÚÃÿûoF¿åׯš¿çù]©­/ZþŸâoíHžòÿkÇçSÿ»ö›<­5é·£õñ¤›vÿ‚óõ´+¿Ö­ë}œ]Þ¬ºG·£ÌÏ’¿õÙ}ëõâàxËÑ=5°ïû?†U‘[¨‡e/!Y±³¬ö˽©õñR.¬‡X—W·#fógÞÇïѤü¿WÀÖ{„Ÿ–¿4~îÊwùDLùYK¿mút@r”[;?Ù5í=ïq5ç÷Ÿ%mj£–TÏÈ)-O»f+Þþ9ã>é™ €@E |ðZbiqæsû¹p¶XÝ5Á.ý^ʪœ!œŠ4»p^K¿ ¿o¥·œ?KEPS[ç[÷œd¹æ¿µæÓú­¥žžúçÞ3Ià[ëÓs-fxz¯£·?#ïV¼šxMà{²Wq°æs*»b­§ÆoRg9¥Rpј)¹£÷}Ä£@žfyŽäíVZ}®P"ûGK/¯Ý'Lg,¢Då®Ä~Ëý‘w°7`T°¬nÍÝÅÒ¼ª½eJé% ¿uŸàˆa¹}ËÁ;p€¶F@;Ÿ'óx¼å[ÛÇß­yþMø®L¿|+ÿ­ãü€ ×Vž÷ž·–}ÖTB­ž³§<¾Öo£‹|[õ—ʵÜÚçÑûêâœÞ„'½Føä×"½–óÙùÜ­Ý€"€"€`x@ø¦µ(ž@uVE@Ä¢¤ÀK¼/UP^î€*x^¬Àsa‹a€ƒGEP^ʇ Àx/¥{Àç³m__íùH#ÝÜåïË<{ò©ÕËÛ)ëÐéGõûhÎî·­ø}2< R¹©’ö‰ý¶WúpÅû¼uì‹ßèžq)¥ï–íûþã|~n5RJ?êÜ[ßÚk©ÿÌzæ÷B^†vþà÷ mVÌ’ÐÎçH%ÜášqM€¤¿ivAH¦…¬ÙrÌÒ^©ßniõ¬|]*‚tß÷àÎmk}w—öήgí~Ðúï’©IP·I°çùäŸeà,oÀ– ‡Y飬¤}[KX­ÞoI(7Ño·í·»\0ëVîΗ´S³ô£ù­féhÉu?¢¬27–*–ľÕÝÄ{%É*‘\Ï–¸æš¶¸Á{˽{¿­lm¯Üoû‰ý°;ûGkÏ”º¯øà i*!?XŽ¥;[óD´\ÐÞ:Jnt¯kÝ#×ò¼¥6¶úÍâ©é½.#Ó&fE §âšë¾&Ð=VxMјåþ×\ ù÷¥+X|jÏyÍE)åoµÎ’`x´êÙ*÷©ý– ãüÂï÷[„§ ¦p$GÿXûsŸ}¯zð:¬×u¬}¶Xë½ÂKÄåw£íµ”]þ/)>-¥ âºxúÑ£è‰(´^Å`dàLßL!1ð¦ rŸÜo-#.¢OŸØo£Æ«¥kŠÎ6ñ˜â-¸úÁk Ëù;»áKoÆê '-Ê•¥Í^åê÷™ Õ„¹´à¯WyXæ,¬—³n?Ëêvú­¼N/î·+¯‰¦”h Äåo,üàI‚ ʲ]¹½3]ë³®Ë,Ũ,+|± wžÿ˜NÈúõz!޲­ŠÂÌu Òb©³ßt=Ï\»±B¿mô›:•R³èµ©ð3ö’üð”_ö.,…k„`·æñêàÞ°4ZÏ}9-˜Våè´ ¶ö(mú|«·\KÞ­ßFÇðd’Oì·Ú½2…®Ý·µvÕÖCôôƒkÜ8ãÁëÔ[‹Ì´ô¥ ½%tjé½nlí·Ò"E­]Þöz­sK¿Yû§·ž³=éíÇçŸgú{h¿µ_è·göÛ¯Ó•×ëNõqݤÇÍú”èƒ'¸k·‹ÝÔôýp–ümïø?€CÐ!ÒþŸ\ŠÜNhU®×¨­N–VÃjçï¾w:Àã'Êœþ>“»¦HG¹’¶<xN…/1Þ€èÅC(±t²…°æÓz_4?_K_«gO‰ZP ‹póÖ?"}« µöŽ(@åý[öpEÀœAÛª°7ê’$ø{Û‘¾çzµîO`íZá™8Y¸28ƒ·¬¨]—¼.æ¨>)wÏjÕÇR®–g«½Q×]óX”¿Š 8\€¼að]-ÈÆõ©E¥½î^¬Á/Iðô,Ⱥó®Õê¤ÂkÁ{®û• öP2&yzæf¥ElQs¼Ñsŵ – £Ý3gî úáÍÆõ*ó”ú œ‚`Àõ°Å0ÜÆ:Ç€"ü¦ `UFÞ¤¿Ê‡NÚãlƒí§¾Ü7Œ'·÷ÌÜ:VÛ•oÆâ°YóÉÞ÷ãÏt£ËÕ®‹çA)pç¼VžußËk™­ëé}F"úí Ô®)ë=Vf &i5|ï2 $s¯UäÆ>R>¥PÔvXŒ¾æ5%4"ý“6Dš©L× 8—iA‡f?ÜO±ºïlí­îòöÞô\‹2¼ºE?ëš‹à&Í…[~ß Úã±òÓ¿6ÀF/òôQ«Üšµì©gD{5ëlÅ:Òµ~†e9K×Qª—'¨TëþŸ n¨Hƒ%xNÏNwÒÆ>Öüµ¹`opžQb)×;×m "ÔÓ^‹p¹±ÒhôÊž­½Fõ¦ŒÞ)ïýÖºÿ¥g,â9ÊóÅÃp#EÀ¢ ̲#ʲ¼‚4s.4²žgæ5Xׄ´g@¯’iUl¯Jßjç¨ =êþʇ¸Rf΃ÂUMDTÅ+Ûy=ôû¯ãH¿E³Z-HÄñË:Ǭü¬…C#؈5ZNy´ú3j  F=]µPN[€g}KÅÓ_ÞôQÊ•%””¦öÞþU÷”4–ÌTü`‚G ×5m™ÇnÍEZó×ËÙAlzÊÕ\ä#A„FÚk]ŒÙwµWÈf–[ëË}5’>¢žµàTÚ‚ZO0«ÚŽ'Ÿ™^6Ä·,Ö€e•2ĺjßÖþÕë;óm‚™ýµAÄÚÚxå}`L˜¤PùÐNw.ïÓô»Oç¯××ßßß×_ýeî<Ö´gsþ»ïèÍSY>E¾YÏUßÝËk}ÎyøüõûÏïÎÏûü¿ö}ëÿ¥ü=ƒw¯Þ­~·›>eéÊÌþ¬´‡wn#ä,^ÈžU”„ýiØÏÇê•ã½ï3Œä?£}KzøÑÙ’~~§ï â#e±)oúh¿{’þ~òU­xl›¢]v·‘WØ%äœìd{X̺ïJÎ; rêU¦ˆžžÓïäd¯üY«ŒrboöÕ¡øãõvjËú¥Qtëà»#zò¯®Òh+•GX{nMžÞr*êû=ë‰<÷œ¾Ö~½6뵯·<³ŠÊi¼Â PÎÜzúÐj¯H?õêgæRpo«Eig,õmõëR¿Rõ¯;Ö+ÿí¾‹œ/uj¹õsd6q7ÿÚkÏ Õ.jx-Ïõî!—~VÕ·5hYŸÛ’›7ÿ–þ(Ê£4䞎{6ƒ”~Äðxåïí½~ZÒE?;±–ó-9Xm¢w`©Õ7〉úרSÛ¥ZúÝå|¹#0k9åÜ‘Ô3vUþÞ玔óÊ|jé[õò(°ª³äàíwWÔwdkj†üŸ g5?ÖÆý|² «:£ž¥«fyç-žÚBÁ|¢úóv¶c­},òÿNwÛQñ\€IüþÝŸ??>œÓÓ·þ¾•¦–5ÿs9kåÍ,çˆ|¢ù×þÞ[kþÕÏñõïQù¾”¾ô»H>­ôGã÷­2XË©’ƒCZí®ø>K?ÃrSþ?’ÿáГÈsùðYàã¾YÐ×;ÓÙ ¸k oþÞÛøÜÛ(Gò²è{à¹ïÆL®—Ï‘Pþ·>ÿÞ’ç뺳ϼȶáÞÉKìoÑ–À!z.À.[^ Î0#HÎîA$¦÷8r·K ê{R½Þ¹íÞ Õ 7Ѐ$~T³tEp†+´;‘˜V¥œu*üpæ}ˆÊrhÛÝôæ6oÄ|V|F_9 ˆw[ z£ÎÐú~dV¾K‰YÁ=Bøü³%ýyVì9DÕÊ'Zþï|Zõ*•S)‡>ÔÚÝÚ¢ý+E¬íxTíÚÌü òµòµÚ]ù\€ áŠaØÚ[€#rj3wÀ€>ÛÄX äƒ|W,ïÓô{°"P{¿Ö ½†j·eÄ“”of¾W=o§îÞ–ßõ²~ÿù]ïæÈZ¹,¯q¶êáMßë/Wë窺²KÿïéS´ïªò‰ôGËëÚŠòŸëк)ÖÚß½ùÜ…?WvÎÒIeOG¸»ÁË®ÓYÙ£ÏZMö%ý9wöïŸ[f´^-'W‘>Ú_ž¤ï­ëÌ·žµ%½I•QÆšíW´…×A÷ô÷H>wàç ã¼cþww>V¯G¦±ºJ”³ k;î ·L<n.SZ§ï^©›ªvŠþ˜‚Yf0¥%šèÖÁw‡öäo¹De(XŠ¡Ü#Á‚,u :ä)¿·œWeºÊ€)Êr… š9[Ú1¢'-=ÌÔg…zú©ÒçÞ•èêþÉ'"÷+VVêï·wJÊU2gO­ôž¼V¯Hçñ–_eÈûìÌx jvú‘Ñ+¯žôúW©+úéY×-ç-¼gC²õÙ›·]TÏÝÅP\”·âÄårG`Ö²‰ê¾tKpžìü3äì©×݃2Õêæ9# Z¾Ì6À ƒÝ R4«©òñ:oÞx(« áÚUù)fÙO¯Cz畃?«)t†°oü'ÙÑØ-(Ë|qù+ŽFõäýh›¹ŠüWqÀƵQ?Ÿ¬F¼ê`ψ‚÷f‹Jã5#”s-ŸA™¢çT§ª-NFm‹Ê[OµD¶zíøIÓzo…™éwùF^qΞ-ßye`'ÀGc§vž¶"íL–}ûÞÒŒ5ëIUµ"*ƒÿxE©êµSP¦¨ö:{ÆÊDËÈ´ôv$ýí~.›·Ý[[8ž|T«Þ`MWèsTJg¬ùÌrzwxû‘¥mTùìÆ.Šv¨'Lo´¼™odÊMuNu@·5ÐÀ€#À ÙïÝ.ú¸½#÷… Cƒ3#@¾šW_ì€xE€ Cóò½êy"èÐäðœ¶Ípž-‘ÝY¯~²Ü3pß7ÖyN«Þq]§óí€w™y–ôç|ßüyðüþ(ëUË[•>Ú_ž¤ïߧʯÂí`{v)çJúT³‡*;‰#ð"èÐòKAJׂ]¯ƒê C»ôwV?räGÐ¡Š—EСñü :Ô~îl烠Cê•1»•Âh×tTUNoù#yxû˜5(S$– —Ã()A‡âò!èA‡TéG 3A‡úõ*•qä,•Eÿk:×û~´œ5»æÕ“V>Þ¾d•g´¾%{¨r nçÌòŽ:Ô· C}£GС~=Ÿth…%è•˽®7sÕnTow“¼#±DGСëëEСgBÐ!ر]¸õVA‡‚Þhæ :’A‡:ä‘A‡ìõZ±Ÿ^)Ó•œ‹Þ*í+37‚Ù+‚å´ A‡rÚý\¶Ýƒµt£fÇý´×ŽžSìŠrªì›z,µŸêòß‚ –ËàÙò%èP,Ÿ‚©O•lï0è@æÀ[[¥¹C½nëÀ}ùƒg Ì žËÞ¸êô~iPQå·ûO¼…Z}G^ãêÉój±´/QèDŽÀNƒ~)ÿ;ÕéÕµv#£"àJfð ‚„≮’]—¬•¯M)ÆÕäùô !*L÷Tçš :µ™¼ú´v«œÙÎÍÕAB@àTÇ6€[ËïD½Ñà<ù´.}j9n£å'HÀ¦+5a‚êØ…Ö³¬72Z뤘$`sG€ :ו¿‚y5yFŸË€ °®Ã‚Õ‰—År÷þ÷ŒÙ»JRš·Þ«:‚„l¾"@P¹Au"AKJËåÖ|2V°.Ý C½Y>ÜÄ€gñƒp¶Á‹DŽ@䔿7xÎÌAc•Á‹A­/빈Up˜Z^8Ó;3@ïk˜°#0z Lé&8È™¯,gEy"÷"°*ÐÆt€zÆè½_Þú7­ôŠ FžàEªYp4>A©,ŠzÕòiÝùß 4e•?Lp¢ƒPÉ{fgÞ =Þ Eµ²ö³'hO+˜ÏU³ïÚM}£õªýܺó¿¶Eäm¯žó‡ó°€#  32xÖ‚óXò‰°2½AúÈô)»œ½´Ä1XĘ5 Dœ ÕŒ}÷¨6˜2°ÜŸmîÈ RdIû™9÷^u\á•ɨœ¼A”íeuX¼«8¬ ®(÷`kQñJß{ƒöŒ¤·ž]¨€‹óñ€´Êß}°'·HФÞaÄ‘ö€ˆ5àpb<‡=yAŽü3Ž€|b 8p ~Î3ÞU±í²2È÷.ñ,vÔì€xEÀ³§ÙõÜ0§0Ë÷Wï;«Ÿ·Ë¥;žsÿRèÓM‡½ï?¿‹\¬Tû)Ïàm= ZëKOÛ¶*Õyu9X/ÊÚ½-gµCÆsï¼5ü“Ù÷}3÷æAf±Î1zÿþj²/éÏ÷Û ¥7¾?ÊzÕòV¥ö—'éû÷[*Šg\-ã»öÓõòNýîRG Ûcº‹—9sð\½|«w6ïµ×úU êµóM—Ê6±:W+÷%øî()¦'ÈŒçY– 9­-†Ú÷ÁmÁ‹Z1zr­—7èP©<½¥uO°£ÙÎGv¤ÄUœÌ–þ”Ê¥Nu…>+ä±e*yîÞO½v¸6¾ŒØÕ¨}®ÉíîÙ«¬)r-˜Oä&ºóßyò·\Rd-ÿˆ× ^Ô„<ù´Ò{ C­^‘ÁÒ[þlC¹xÊSoÏ…N#éG ”2¨W¤•ú°¢ŸžuÝrÞ¢%«-‹ès+:è®ý4b‡AÏTöß"·;žÃŠ5påò¿jÆ>+ÿ 9{êÕs4<ù«ÚQ™+òaËð´E}£‚2}¯ž£Kèªþ¥ÊÇ㼩ä ÎçŽýTuM¹*(\4ÄûW– :”%ä]‚ͪWo"«^ÊvQ.ë?Åj˜JOVèG£ú“ s÷~:zþevùïl_~¬ø|²¡:Ø£öJ=³Ee'œä§vbV•ÞšOôàœêä¿ÅɨmQyë©ÖƒÈöB¯-A·fÍK¶Ê* kÐ-µ<ïÒOå·Œ/³W%î>ÉpŸž%øŒåyÖ|J߫Ӽ¨UÏ¡(U½,A‡JiJ‡iTrPvxË¡¢Œ•‰RÞÞàK™Áš¢í~.›·Ý[[8Þ `ŠYy­Ž^ѧ"¨Újý4b‡{úõ\«M¿ÂnL_|.ºj™kg ò–7óm‚L¹©wªèzÒ™åléVŽ ÙïN·[Þmæ‡#!GîË"Àx½^kGóòÄ ØñèYe~î1I`GRž‡-ï÷Ïß‚#©Í`ܸ"¨Au`ÙÁèx½^﯃˚ÎZ­ÍŽ…Ë °ÓŠÀ7»àà Àƒx#€(¦ CÞ`Þ :–|T+»ë ^)ë„ôaøþ]Iû6¤ÿžá½ž[Êç8ýûŽ$½ý>ZžPО‘véÉùÕøÞû\Où½å< ûÈ`~ÿ¶¿¿ŸŸ¿?ÎéÎé[ßJSËÇšÿ¹œµòf–sD>Ñükï-5ÿ>GåÿÊïÄ|JÓ«££½Zí¢ø>Kº²ð¶KMŽ‘ö=ßg†6>œzî•>‹Ü7 zÃ>zg2#[wªãÉßz{–'½‰C”þ=)ŸÒLpp꬟ŸŸ¯¼WßݾJyzò±<÷{§ÖV‡H?_Žò(óXmkÀk`"A0®Œb8RÎtìlfiqxUúì|DN€uûæ*ý’Á±°þxò¹¢^lÀ¦ü¨féŠ W h»ëPåî8§ŸÎ@{$<ד^à´dl n³Ô+Çļ2õᦿBvW­ŒyPÁh}?2+ß%¨NTþ—ù©½ªuþþÝ™)µÒ«žkÉ«5K|ûÛ«Ö.V=êJûZ^ËëµûlߨSV˧U¯R9•rX®†üY%F`Y¸bàÁ°"ð`¶‰5°O—Ïnõ_¥¼OÓì À†+žØÐ‘8ÒµÛÍTqÎG˧ÈwV|íŒçön"ô€üÎÇúýçw½›kå²¼fÙkOOz•Üîæ\%u¾«çÍ4·£ýÝÚkºf}®ÊŽµì€·<ÞI#ü¹R!K'¡{†{µ÷©†zDÞ¥|ÎýûçÚsmÞ2ŠôJ¹­¨JÓuí1Ì™)VåÖ`í±‘öïÙ‡š]Q>·–Oo‚â-Otäå'S²ô.†qg±ó@™ÕŒ­ä–ÙÇΆþ´o__I—Ïz5rëêÊ}¬–>}E µôQó꣞þù–^ðëlMŒÅ(#Ï*ˆP´¾µÙÙŠ:2 ™é¨fÎ¥v,•Ë»ÙÒE«L9´¾÷”?²‚4"ÿìöµÈ-:©V`Ž£Ôj/w¬Ú²‹Õ#ëÚ’3`É¿¶ÜëXµ ,Ïõ.õ”~VÕ×Ò‘G;z¦Á>G€ôœ[ÈN?20z—þ¼úÖÓÿRSô£ï|-òŒ,Zû‹zð²–³'ÏYùg;ª½nÅÅwÊçªöêG.³Æ—©é<Ö@VÇʘ±gß÷® ¾4²²2Ú^™¤6²$Jƒ=3}¯ž£Kè*ýWåãuÞ¼ñJfÍN³ƒ­Íæf±Vo9ŠqÇ¢ŸgT«"–e‹ƒáÏk!²–˜f•¢Íª¯²=XBŒ·ãˆÜTÁ©f¹ZUgj«—ùXÒgç¥p…˜„mwûöcU‚Ï'«¢WQ‘Ùè¹S÷ä©Räè,"c»@?Â:Ó(m!yäåM¯r®,AŠJiZïíÏÒ©’-‰êÐì`MßrVõÝZ;fä?Ó PÛ«"êzEí‹Ú.¹ÏŒ &¥ï{{‘Öü­'[3ö½Ï­-‘ì}Ô×zQý>õ÷¿W=ײgØÒ«‘ôŠrZåê NÕÚÂñä£ZåPí‘*í€G>–~j)›2ÿ™N€eÐòÞ5âÙJœmg¼åÉdz óŸ …”Ë’wE¹Tû´ú¯^ÞÌ· 2å¦: §8@Û²'wëûÙa¯oßщðŽ6ºkE|ö {1ÅŽžÎqpîè@ÁÚtàÁ†Gà¿1 àÁŽàÀù×=l<Ô˜y…(Ì­À€GñÏ+#BÀpÅ0ÀƒakGïüþþþó:O—Ïnõ_¥¼OÓì ÀüëŒÀùR¡^'÷ÆÊ®Å9ψ¿)Ÿ"߬çΨo«]¬Ï;Ÿ¿±~ÿù]é«Ö÷­ÿ—ò÷ Þ½z+äv7‡9<§mwœ$ÔÆ¤§èíO¶€{ô#ØÏÇ:{`–‘ÛVÑv)z›…|>ºPÊÿ;½ú¶ËZÞªôJ¹Ýu㓯bµ ¦»»ô1˜§¯ß}õûokßß™4G ÛkºË¬{Wvè$êÕ›HûŸÓïd\²WÔ¬ÎÕjvàÎå|²={r»ýñzY–%”Ú²ŠçYßK¿Öük¶´TÛÊ_å‰Öž[“§·œŠú~ÏÎÎ3õUEÙ®p”3œs;–ÊUÛ’ˆèOß2ú‘G½2zÊã•[Ikß+Ëé-$okéMk[Îk߬íR²o­­B:£#Pj„’Ïž~t™¦ä Xò¯)]ÏpÖ&:@XžëÝë.ý¬ª¯¥ãvžÞž¹Õôœï¹…ìô£†ÙcÀ¼úÖÓÿRSô£ï|-òìÕ«TÆ‘³N–þXÓ¹Þ÷£å¬Ùe¯ž´òñö%kŸTØ·Ö÷QyZ'F_˜é “zÆ®ÊßûÜ‘r^™j&Üê¤5GÏ2(ôÊR¿ìô½zŽ.¡«ô_•jÀ¹r‰xåryúê+}½™¿º_ÔtoE\ÞÈ83`V£Žî'Ï®¯²=žÚÁí8"7Uxq”¯ÉÛ¥·o=ÑñcU¶Ï'KðŠƒC³tÕlô¼åÑ“§ªCGW%2¶ Tå¬^|m É#/oz•suÞ^è:ÿ¤iÄŸ¥S%[âY]¹@µ*·²]z‚sv^²ïÙÞÒHXˆ.M[ö±{{‘ÖükJQz5MíÀxŸ[["ì+êk=Œ¨”]k/.ó¹-ùXôj$½¢œç¶²¤«¥÷8*Þ|”«5ݨÙÏáE‹œkΧÕV*Êií»Êm³ g=jß,íR{=Y)Ÿ»òŸ …X~ÑÍêÔ{‡ú¯^ÞÌ· 2å¦:­8@Û²'³êFGஃ(8l r"N@éä|-/ö´õNÀŒ°õLd€MÑ·J¯¥AÎ xe9+ʹ—‚U€6¦{Ô3ÆHd=opžZXß«‚ê¨fÁ#AjwÿÔËÜ©Õî^ùÀG :• ¹gvæBç ’S(k?{ƒê\,¨å<Ônê­WíçÖÿµ-"o{õœ?œ€e˜‘Á³wÓa+U¤Õè Òç @ùd ºž<‰c°˜#0k@ˆ8ªûîPm0e`¸?ÛÜ#  yMû™9÷^u\á•ɨœ¼A”íeuX¼«8¬ ®(÷`k‘ÿJß«‚ÞXÒ[Ï.ÔÀY‚íDW"ò÷DXŒ ²ÊTÙ¾W ;œÏáEO^#?ä €# €˜ÁŽÀÅÎ÷¿ Ž8€ŒŸs§[åێƉúS^ô{°¢žvdÿÕsÜÂe̼]itÕÏ­µ‹ç®ˆó p¾‰°÷}íy–‹jÿ÷è|$6†Bnw Õ¦œßXØÈ˜¼7ÌÍèŸ:Ã/9gªÃ‘¥|΃bíÆGu›·œPEz¥ÜVÔ ¥ƒÉMkv@vç¾Ë¬{çÙÞêK¾êÕ›Hû[œáÕgôYmr¾É]¨-ážo 2ãy–%HŽu¶–ÜÆûÜÖlÙSNE}k³³ ´úúêl'@5s.µc©\ŠàQ–~Õ`QG d ,Á|"7ÑÿΓ¿å’"kùGËsA~Tõµ ®£ƒ "*¤Å9ðž[ÈN?20*ƒnEô¿ÔÇýè;_VØ6r,BÖ Pñ¬YÁsT×òª‚ )õLÞsF êdZÛYé{õ]BWé¿*Ÿˆó‹:çìhv3Ÿ«*çŠo^`ôãí¨¼Å2êDÍ « ¹üXÀç“eȯ:84epÄzä©2´ÑU‰ŒíÕA¹Ú<ë[*yyÓ«œ+K0¨RšÖ{û³tªdK2HXˆ.MG‚ Yžgͧô½ÚðDž[[" ²4R_ëaD¥ìZ¯e>·%op¤Ì`J–`P–²yƒkµ¶p¼Aº²V9@<}]1l9¥ Ú­žòf¾M)7ÕýŠ´-{¢Z}À&$9w¸Dá)º¥(WzG`GîË"ÀxÿÉ®l)êÔ©IEND®B`‚sugarjar-2.0.1/images/subfeature-part3-rebase.png000066400000000000000000000221331501047226300217370ustar00rootroot00000000000000‰PNG  IHDRüBÌŒsRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé  Þ …‰ IDATxÚíQrä( @íTn”û¡ÏäýØõއ „À`¿7Õ5‰Cc ѾmÛ±À+ùBðvCà8Žÿ? óvù¬VÿYÊû¶~Ãx°ûvÙ#pǶï»ú!צMÂô{ךgdùîÌwµòiï“*ó;ÚëçßÒû¿K×K¿çò·(ïZ½Kýyöþ3ª¯bFÑ#ÿ(¹•4K‡ou_œÉKƒF‘DÈ_#×Þékõlu¡Gõÿ¨|,Æ[¤z÷yÏä'¢lQù¼m\‚ C ÇƒÔ£ñïîPÚâÉ?Ò­ÿ"¼Uš·&Fæs—î4ð{?ïOÜcùÒç§W§ˆ´ö#­vïlôLë•YÔ좵®ÒÎnïÆ¹¨ÿ#CZB²ÈÑš>ÊØM—¤6жWdiÉçZ>íÒ@tÿaȤKœµ±à®zÍ–¾u\‚ޝ«J³®U[‹Ôæ/ ¹WÓFYÍ–{Iå,¹Q­›´¬kÝ=g€¥MEÖþÐbŒXûgKúˆr¦m^+[)½ÅP±æ5»—êh5D5òÔ´—gœ©É³×}5yYïûôqé­ü:Ph5wÜDlrZ­ŽQ+gm/o;õ>„+¢½¼ù´–·§¾Jdüœ³ŸCÁ #ÇwÚÞžÊÑíÕóUÀõå¤Oú9t6à¹}Cà_îØÙz¯Qe½kSJËë^OªÏ]<½|]¢¨à?³ j ²Ï€`)=Wzð$èÿ°:ª Cµ 'Ò»ÕÒßr¨%½eæ-Omö׬#B>VE°”yƒ¥`PÀí†@ëÁéµÜ@Úd£|£VFKy4ÊU|©µ¾¥Ÿ-,…`)U@i Š¸4JVŒÛ®1R¬§gõVKñå•`C`f»wié-g‚¥<‡¯ÞŠ**Ê*ÁF¢î9Ãë}K‰5J–S{<Á%¤õÞÜf©Ö µuYm^-Áa¼å×”¹wp$‚¥, ;6m1 ‚’õÞOžÈ¦ó@G‹{P8l< † C¿7/Ìçžû;‰l¶~1‹¼VÙüÇ&EÀ#]•Š9À¢†@zÞ:›™Àý9À=¹ƒP¤¥„ëÿ÷®”.ŸˆôéßF•Óª@®ßo•d;–ò—ò³ÈÇs\qK}=í%¥/ÉA{_ܬ×¶-k ö¾³Æ[` ’³J0Ÿˆ`;ÞÙ¥”¯VþOÂcí'µ<¼éSÙYËÞÒŸ£‚tÀË (¢æÌÌ'*HNoÞ„'êøãÙ6®Zû*J¦1zšwÌpfžYiËò¦uk©½jG ÷4Vzß—} à!ä­•^Œ(ë FÀ5ˆG¼)ϪAŸZäfÝçxªO:ë°ÕÑ~³ói½oÙž¥.o ÂSj¯Z]­ýP+‡ˆûJrk Ò…'àÝpÄð¢¬Ć <÷Ë ™†À”Âʳ:‚ð 7À€áˆa vj’îÞ™ü”`9­»ÁÙ%¾´Q›ì©ÕÈò”NòKwŒK×ߨÔ$¹Á½ý!¢ Ø7€!UŠO-åXdKêO»@Ê·dçÎ8OÔ–ƒZJç´çfzîz«"ÔÎX¢åyøåÿ^õ­¥OƒYÊÕÜ÷ü=×GJçíç®iëUþZþ­mlmÇšœ[륑³6Ö‰¥]x]@iôªsGp˜ÖYÉÈàBÚú^ËY‹X€£‚ÞH?k%ÕÚ·t_éP¤Úõ´ÍsåŒFTêç‘A±,ÏWÎÚôÒõRˆ ÖAîºU‚ì\H*cnP¬E¬Õ7*èSTŸ-Ïóç–à?š~Û;A­#ëÛ»ÙÓlÌÌç®à0o Û:[4ÁYåã9:yæ~UÛ?ÒR߆mÔ8€ð7_3=(³‡yã€ÑÛ;³ZhàÚRŒ§ü3 ÒÔ÷ìÈêÝkñ’Dô“Z.ŒÀ#°ÝT§wp˜t «}§·ZÊS¼#‚)iäé ’£Ýô¦ ž“[ú°ÌÎsrË•3J>R¿j ŠÕ2÷ÈY³®¯Í§¶a³%XS)Ÿ^ÏÀÊpÄ0À‚^›Ñ ku%YÛ\ˆ6®3ÿ’wC^EÈÉ‚l´€ o&4îZ¯\¡CXÿDÖ‹À+Àà»^}ßv_£c¥`<üÁîÝ™Fœóo :ô¶Á¹wp§§æ«÷”Ľã¼ÌXiÑS7õˆµÈè·x0Â,Pé@’4Å•;û\“®\Úû^óÎamKõª•id{YëëÍ_{½v߈þfé?žûZä•Þ#·ÞùKõ’–ÌzÊáîv±Ê³ô|µ¶oÏq#²½ÀÏ÷H•5ÈIDþ’õh †“*è¼!– C‘òì1[‰^D¨4û°”ÓÓÿ-Aº¢ê[‹Ia B¥é[Þ RÖ FÞvomO}£‚MYǽˆûŽè°°!P f‚Å/çÙip^Ùi-§µŸ[ƒ>EÝ×Zß»êÕêýŠCT0®Ö Ïü]Ï]®îžàW°€!pmä‘A„`ü@é9¼ÁH»Cn½Ÿ¯ÞÑIg+'ýy̸á_­ ¥  ¢ ÞâíT× "½‚Õ‚®¬øðׂ±ŒF›šm'kÀÓo=Á‹V Õ;ŸÙ‚•­ö¦Iïq£¦/0&÷Xƒ«´])YüÒ†MðËŒ¢TGé¾QAN¢òñ“±eJ—,¬ùxÛ«5ˆGÎÖûZf’š ]­õÕ:©¨•²5jiw«<ÁÊZå&­«GŒ{wŽ£¼Eo'äˆá¬³Þ›Kžj²)ù¬ÔoyF&5fꜽ,È'w>,oä@¿Å€ò•Zxà`³„vzZyßW€qð\Ë©vÞÝ¢¥÷É£\L½\ùµ|ïZBXmébTyµ÷ɼb¹~þÍ{p–æõ³Z¿³¤¯=wo\‡ÕDðx@ïYEé4+k¬÷uf€«¼N•ë‡×]ϹÌ×OdÙ¥¼£Ò{Ÿ»7õ_ë±ÖŒY€!`œÌžÿfÝ+yHÍ áª#½Lž~Z{=ï }E’aÔÉvŒ0+ßš¢Ô™sî~ï;ªé;øÒû®š%ézîLëÖ‡Ôz_IžÖrFÔWŠM ½¯túWúýZ›ÕÚ×Zž»S”Ñ8ˆœéÖúC©½<Ï©µF<ï9XÇÄÚs„Q·Ò€\úÙ3›ÈuxKþÚÃ4¢ƒ{xî|&ª¾½ƒÌx‚œX‚üD¾ÏlÈ=ÎXŒëAFÞô-ŠÅ*ësQ{NkJÒû¼§F¬WGzX8on5îruEÅ#Ðé=Cˆ*çÈ|Ÿäë}Óô›ðý àKù—ò«•§0³´áyÝ:;ö‡Iß¹ÖA"˜@2–nÛvlŸÿþO¼þé˜Oî;‘÷µæ_úýc¼oæsrýýú¿öz._O>Úü¥¿_óÉÕ«–ô¿6O½re«•?÷»¥<|øðáýÑŰ®s‚ÜäQùü8êõ1Ü'ª< n標ù5?)ˆJïuî»b%<5˜€~i`2E×}}ý¡ë÷-'ÎiÜã«).­á‚B€7bß,øI~þTfÏQ»æ?‹§×%WyV‚iZm•YwÁ_ƒÛx°í™ÞR~Þ4€1A¥×*3êRúM‘Þ›Oúë}’ë?Ç’ÿÕXøÊßhhh¼\¯KAN4ÁdJ×[Êåñ{ÉÕëiÁ”þk6ïÖ5t°Ëù6{B"‚ÛP~xGîÅãaÊxàqüµYÍI:íôäò¾­3î‹GÀ²éY³¬Â–û[ËÃÝÃZË÷®µÜÕ×Àï¾Oª®¯Nj®Ÿ«h(•«ô§æÙ°¦¯=woÜ“«óLr°lLp{zÏ*rø|ÐÎÖ*Çz_g¸Jpž\?¼¾=[·¿~"Ë.å•Þûܽ©ÿ^ßòˆ¸Gk°œ!p×ép̺ŸA”œWPrÑ^&O?MÓ¯dôöüi+9¬Ê·æ(ufmp«Å¬ ÆRZb®{ƒÛx,}é¾’<­åŒ¨¯<‡à<íÆGDYF‘3ÝZ(µ—ç9µöÏžîõÚR‹ö¾¼-ÓÒ€\úÙ3›HTkþÒkm@’.ïÀ«¹¯u 9÷sT}KJK{ߒܬù—úODy"rË€]:˜¨VžÞé[ŽUþÖç¢öœæúJÄ󞱚ý%9hÇD΀i »\]QçÛK`ïó󣎩mñ¬´äó¶à<%%-,EÑÿ5rí¾VÏVzÔs•Åx‹’C¤<†=gŽ×Ãt—¢{âþŒ‘mÆ,Ì×Z½1.£ò¹K‘ùhùÒ>\-\´Ôëd­J¢õ¾yF `½ƒç̜ǻq.jç¿ÆÈ–º,r´¦2Ê5Á©riJ;ñïêû¹1O+ m®yÜâðº¦5ëØµ5>mþÒCÓû¨VÏ}%׳gÍ9¢¾–MTo ÎSÚìhí·-ýÊúµ¤(§&€U*W)½ÅP±æ5+—êh5D­í2ÞlÉB¸§âfKÑß…º<#6€®Ð¯z¾MÐSnQ›á"Û¹çDaåþ/5P\s0~ ‡yÛk•“ö8–7à¹|!‚mû|ÐÏàüòh]×¹Aíçç÷ßÎké÷®iÓ4¹{ÕòiœÿÊãt[ž2¨m6JÓ•¾çp jäS临ôHîºo¯òÕ§¹=£ªËüÇ·40hŠÜ€–t×ߥŸ[ÓákN&W¥ÿ¦Ù÷¿ƒãøó7??ó(Ç«)“+ý»ŽS€¾þè·}èë)ZÅVs§¶(È{UöZ/‚CV¸•of1hK˜Þ#`==ÂE/-%œŠZ;p––$VÇäf¼GF¹ÂÏ-é[î{õh)g.¦¼¹ïI÷r-A¨jA¥ÒïxƒöhÏYh-' 0r§YfÔçÏ’â>¯å i)¡¤ìk{ Ú×ÖúõÀÁª´çÂd h”êù{ú÷=sÝ“¾å¾ûVvÝ[˹)Œ„­RžÒ»598»Ikªè R– U¥h”ÖrÀ CÀbÔfÜ©B6Ïh Š\›O³G@róKòi\Д×lÜäØQH[2,,3܈ûnAå<‚òJïôxžQ‹¡a½¯7øAu0fâ:Kž~Ã\§ýáõ—Üà‡ ÔjnþÖû†É¿sþVã«Ñ0Cðœ^ùpÒÀ\¸ÏÖí£6BË §ò³*AM9VÙ¸å6,º#À•ÝE [_¥~t(ï)Ã=¦-#€»‚J¥{Rï@Ï:@'@ªD¯ Ûºi/b“Ÿç¾Î‘Ôž.=c §PZo—ÖÕs¿kÒGÝW3«>”i¯Jøêå¸þlÉ?5lƒ´õfÍ–à9µ W­A¥,e-åUNˆƒ#†·¾§/Í`!^ž—ß_ÝÏC0^'Ó Ù†L A‡6Nzã¤;€÷â:”*íÙÿÚƒ¤vjïXO:œ~Ýv‘`8¼ °&MA‡¬Á¬3O)?éú­ÁˆF(}€`‚yB ÷¤ËI}3B0ˆôXƒÕfþšJïÿK®þˆ`GÅYyªl#Ý<8Nú7o›RþÖ÷ÛÀ`XƒE*¥·ü\*Oã —æÁp¬AirÊÜ<dztCÀ:Ó²²bÝлŸOýo×4iúÜß´é£>µ:ŽÎ‡>|øðý :”›ÙkÞÓצÇ#OsСnÁ÷LïkYJ¸=§!”þMs½”À2†€5èP:›¯½‹î9H:˜(·Aº.!;Ö 7­‘¦0,€Ç`UÔ¹Ù~î$BÉph!wú]zBŠi¬€52¡IÉçÂ笌ûÒ¦¾óoÚY¿%½¶Vï€t~º õh󘑦“#ϰ¤Ø,˜*nïfÁR>6`m8bC0@EÔkŠÔ¦3¬c¼u—¼e—ÿçóç£ù~ú±ÜÇRþâFÄóôæÑô¸oz"õhŽŽ^•ã8²}Zº°:îs¤“ö"ËD¥7@¡ð·íß­£ÒµÓXè¥ëy×ôšŸéÐb R$)rÏÙͳö^Þ€ñd¯ÀÙ¿-×^ëÐ)Š< °%fAN±7+´ÜŒúz}»üý~nIßrßC¨G¾»ž¯ó"¥ öÚŸRÅ ÊãÉÀPÌŒ¢SmHâÚBî÷P4Jõü=ýûž¹îIßrß½`PÔ C_©Ž5W|ï`P%Ã!wàõ†@/ ¤ð#éâæÏÏtÒ–”®6}Ô}­ÞŽŽ\û”4óOŒógo” ¢K†ÀBX•{ú6B—õÉm/m¼«¹ù[ï;¹à1:ÙÈàgÚszoDûùùóI½¥ïËeQ¶—¼UaG¾.¸Ç]ÑDƒâu@€ÜY©b½^Oÿæ .Ô}w{i½]ZÿÏý®IußR^©‘rT< ÎÙ|ª´k3y)})ŸÜ¦ÃôƼ‚9÷lx2wûðú†À«y€!ð:ä€ã‹‘/Àc ÞA‡,})€WY¤ß+(’¾?t}<:˜Ð±ÝIÉÇtð8 €èAòT¸¹7¢Ž¾ÞcŠYho];Òx–r*ukúë©„µë¯6¬A‡Zgòç,ü:#TàÚ˜Ù-Oài^³[®¬Ê° C¥™¼5ˆÆÈè_@ši_•¬7¸Pnæ®Ñ×îk)¿µœÖ²•îÛ0ó'èÀ C *èP)HÅ@ò‘‚¥ÿ7Íô÷D9z‚ ÕwT"?2K¢­¡üÆnDÐ!€› « u«_Ó”¢Ö<šû„¹ú¥‘Ð$hã½wÁH)–zyËŸzŽöþGÐ!€ ëìßjX¸eý>j^”¤¨w½ô+A‡ü ;G@ä§6«¿î1(=ö„Ò³lP?Œå (·ÖP$èÀBr•ÞH— RE¡Í§ËŒ6·~®IŸÎŠKÁ,ùx˽VªW®œ[ ŒÞ‚´«1è0[ä €!ÓB¬ ÀØêA‡ ð`C0²|¶mc37ÀK x©!€7ॆFÀË=ðBCoÀ‹ x.Äx1x0CT|>ÔžÁ¯=Ö·ŽãØö}7+œåM>Ÿßisùä”ÙÏOþûÑåÊqi¹`c@S¯ãø{K‰¶íáoùõÛµm´ù[žß™ê ñªåÿÏ'ù]úÿ>å¿þ—>ŸOù÷Òw®iµùä¾ÛZKºnŸó_p¾–z]ÛºÖîÑŸÑ÷ëUöèz¤ùiò×>»om/>|Þòq/ ìûþ¿ÅßcVq¡ž3û¹Yl¯YûíÞ€Ž”d<• ë!³Ë»ëq}f¯Ï2¼ïÖ$ýß«`KŠ=BwË?7~îÂß® 1éÏRúm“—Ã}K×;»¦­×-®ækÿÓä#-m”òÉ•3r‰DÊßR¯Þ†…U>#ú‰g) ôÁ«=ˆéŒóº¶UΚYwI±ç¾ŸË¿ë ø”ð‘¤Ù3×¥ô{æûµôšëAãgjJbíz­Ïåf®×ïjó©}WSNKù¯Þ³œÂ×–ÇÓ=¼Þv´Ê3²ÿ@°!`µÄK ߪ­†ƒ6Ÿ¡ìÂlý¨|çpÞ'5 n3sîè}ß›ñ(pÍGšy¶ämUVRyî0"å#¥Ï]/õ–3&1"ˆr×?b ¿æþÈ;ØЪXfŸÍ­2Ó¼KŽÖ{æÒçføµ~‚ †éÎ8gæW×}ïñܾ¡îp~ç¼Çx#*u‘§³¾Q óüX•ɵœ-ùŒ6r{zlöð¤2Öô¼ x¤=Òõëßr×,Þë}›gÛçïµuþ-ó·4ýžÌàkùoŽë Š\Úyî½®½÷¨¥„R9#”§Ge_’[ë&ßZùs÷ÕôéçÖþeõFo“^#|òk‘Ö™óè|V«7`` XCžA‡0þPÛO :€€!Å¢dÀK ¼/50^î€x^lÀsáˆa€ƒGC0^ʇ Àx/ŽGàóÙ¶ŸŸzG>Óä:wúý4OO>¥rYëaḴGúÖïE}?šÑrÛ’ïŠ$wߣö‰rÛ 2œ±Ÿ×úÀ>yG·ŒKÇñ§fû¾ÿº~½6Çqü*³·¼3ÔWSþžå¼ö…ë=¤ë'ß=¬ÙœbÎ)ýè|Î4QÊÞi—Èñ_š=£$‰ ¬ÞzLSߜܖœõÌÜ.Eºïû/°rÝj[¥¾½ËYê’ünYÈ)êšaSì×|®?ŸÆÀ(oÀvQ½ÒGÍ’öm.e5»ÜŽÌ}䶬ÜVi0íVRçKê)Íô£ù–n¦tÎußb̲6vfûVvï…|r³’œëY3—\Ó7¸÷¾«ËmæÙöÌrÛÊa7ÊGªO—²Ïøà5(•ÜRÂõú9sLÝÙ’'¢æ‚¶–1çF·ºÖ-zíšw®Ž5¹i<5ÞviY6Qž‚K®û’B·ÌÂK†F/÷¿ä½þ=uçŸÒs^rQæò×ÎÎŽÌÄ£VÎÚ}Ÿ*·C1Îß¡üVèož‚’Áq䣕çÞ»ïzð Öê:–~ÖÌÖ½Ê+§ˆÓ¿µÖWsïôÿœáS3 "ÚÅ"G‹¡—õD!mø³-›{zøz*‰ˆ÷ºï“åV›ÄEÈô‰rk¼jä[2t¶ŽÏ@oÁÝ^EQh®¯ì†O½³oœÔWš:[«ï‘•”ynß×x˜¦&³—QÝO³»¹ùÇëãÅr»³M$£D2 n+`â/§¢f¶3×·§k½W»ô2ŒÒ{…o´®óŸË ×~^/Äyo­¡Ðs_Bn³ÔèÁ÷jÏ‘{7fÛ†ÜÄ¥”ÒŒ^Z áÑšª½Ž˜òxÆB«—Ö·[ò8ŽãÿO”rkq¹ç¼³Ìô­2ö’üò¤ônL•k„b׿ñêà^™iÔžûtYð¨Ì*[—¥½GÇ&¯·Zï«‘çjrkÃ…’|¢ÜJ÷Òü-3t©ß–êUÚᑃiÜñà9õÚ&3)}êB¯)Rz«[únn“¢T/k}­³sÜ´òñ–³·Çàxûçó‰Ïóøï#ýþV¹ ·gÊmÅvº³½V*©ü«Íø,&S¢p×n7»©‘råÛ;þOCàÅtš¬ÿ'ßC–SZ£îkuJ»“s»a¥ë«Ÿð8CàɃ2 Ç/³œb— ‰ó“î¤M¯>ítG¥IDAT¡°á%ƽyã wÐ!MPm>µ÷E¯×KéKåô‘(ÕÐ(7kù#Ò×êPªo‹”ö‡ëÌ0¬Á¤£ ½Q—rŠÏC:Ô!-«&Ȇ”_Tð égk}#Ò{Ú«Ö,=¤¶Â30ظ38ƒõ^Q§.Y]ÌQ2IOϪ•Gs_)ÏZ}£Ú]òhŒ¿†  «yÃà;[å)E¥jmw«Gæà+§x<²VÞÀ5[Ù#‚TXgð–v¿sÃF@'€gm6·‰-j7z­¸DB¢U¡[ÖÌ­A?¬ù÷h¯4Ïœ Ò%öÜG Ã2³s¼È7"h£å€»ùJ;sîŒwÐ=ô@;=¡¼oëߌ{€GÀiÁzÖk¥Sùzl뵞l}?~ä`¶ÒìcDy-¯2æY®K÷Óž y}´Öï¬Ï²ôܽíÐR›²/^iôès»á½Èð€®£”½FãèöÍõÃT)J'AF—½d,G¤ÒÁM=þÒà)t :4b¶Ç¬ûù³´(¯ÀÊŠÌò¹õ´Î•‚;õöü3^ï\£éßkA{,øõA´ä/ \Á‹,2ªÝ·4 µ”3¢¾Ò¬ÇIj¿Z›µ}šíÜH×ú# jæ¬é¥öò<§½‚•`,nmðÏIw¹ƒ}´ùKk¬Öà<­µæ¾Ö5dm!O}KJËìȪ­ý'¢µç4×W"ž÷ÔˆÅp€×#]rRþ=fìQù[ïÛRΑùX‚yqTyFÌä-{¼Æ°Ö¿+}­ž­.ô¨ç4*âg†À ôzØîz€ïRtOÜŸ1²Íô}ý¡Õtk¶`b+ð¥}¸ÎO¯rÔ†œ–¡e–—.yÔä5€õuwpª³M¤÷À½ç"Ê)mÀÓ¾Mc‘£5}”Q® Z•KSzoÿ®¾ŸózNP–òx]ÓšuìÚŸ6iêÄÆs_ÉõÜD¨¥¾–MTµ`A9egݤe€{ÎK›­ý¶¥_YŸ£–ôå,Ñ’6†Z‚n•–p,ùôôr¬Â_±4»!Ö ±m±t…~Õóm‚žr‹ÚÜÙÎQ›&#&Q·(®¹è7Ãzí5Ó«€ðhCžË"À€ò¦œºÚãk IEND®B`‚sugarjar-2.0.1/images/subfeature-part3.png000066400000000000000000000237151501047226300205070ustar00rootroot00000000000000‰PNG  IHDR0Å÷sRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé  Ç´È IDATxÚíQ’ä(@Óu£ºÿêLÞYϸi’Ì{Sí$1Œ„Àèø|>ç¶ä `Àî†Àyžÿ~@fwù¬VÿYÊ»[¿a<XƒãsÛ#pžçç8õC®M›„éïî…5ÏÈòEäÛë¾OÔ·Ô.Úû¥Jàúöúõ]z¿ëßÒõÒ¿sù[”w­Þr{›A€Öyîa?¾GvÐkà.¥³(€qu‹¼sù¤Jüþ·tŸˆ6/¡é#åöV%sÉPš  c'_Œ“P£ÛÞï-³îUÉu« œÖöô´¿Æž¹{¶Ée®8¬ôœ4{$nú}Îýê]:¸–ü¥6çª-åïáhï[š-[ÊQ_iv6ã’[ eÔ=a–Ú1W.iIÂÓÿký­Çsd‘ƒg¬±È§–GÉ£•ÊEóüjòÑŒ}µqÒZÀš\§M-}ë¬'í Öü¥µàÚÀ)=€^¡¹¯u­;÷wT}5ʵU ZrË€”¶…Ð;}Ë€j]b°ö·ZÿÏ=cÏQªÔ4Š·$íXãÝcÑç´Ï¯Fn¹4Öq8¢<°¡!ð”Û)}à£gìQù[ïÛRΑùD ¥A̲GÀkdj Û§Ò×êÙêBêÿQùXi¤FŽ]š{•ú±¥ß´.W|ÏT˜^VèSÿ©tÆ7/"Ýú»Ðê­*y¬žÊç)9ŒÇPȰ_ÚŽ}}zuÞHk?r–5M—ð<(ÔúÀí@¤«v·úÏ^Þžoô”[Ôf¯ˆ ´¥ñ¤G}gè«o¶c³ †ÀùvÅ……]úV„âíù*`úÎRÎÕ_¿ãõA Ø¢`üûF[ï5ª¬Omž‰xKaFÞÞnË÷“æ,»‚k¯5í¤t Nþ´À;½Nµ _,`H3³ÜÁ8¹ï¥ï¤¥Kzˆ·«•'"¨“%Õ6ë.oK 7õwÀ#°,; ¨÷@T³*L Ê5˸B°2€í û`ÏÎk™]å2ºÞ#NŒèç³÷‡YŸeÆ€yP,˜Û•]‹àyo<—.Ÿˆôéw–rJy´Ô·t)½æº·}#xo{iå,åaé‡é™9[îk‘¯å¹‹6´ýM’›FžÒ™$ š .Þ`5Zå1s0Ÿœ‹¼wð¢RþA™J]ÄÀì‘¿UΖ E¥v’Eм¯×¨5³·ûª&U»^{`"@ËÖò`¯Ì'ýY=šòGÓì©WϹw`¥’<½3ó–û®ŠC`:f æc™ýyÊ_ á;‹ÞÈÌr¦Í ‚·VÚ±Ýóµ,ëîú¨òï ¢‚ µx9f1:[‚D­úlcô<èÐÿi}½l•`>¹·*Z‚)µ”_ÊÇÚ½ƒµwÊ/Êõ™È·]¤}¹ºô~ËF’›wYC³ßCìK» ²´yTœŠ·™:O0>1 @p¡½å€€!@p!äÉÒ›`=/^f<±v—vó–Ap…zÕÊäh¥¶ìYÞÞù?ű+ ³G`U¼ú`4[ùKá[ÚÊùó@³!°ÒN^6U!˜§ñ ÌCøÉ‚µ³åsŠutî xMþ-ANrg£{ë+ÝWªW©<Ö K½ëUº‡%X“¥],åŒ –¥‘iKð¨ž÷,ˆ·65¬vÔ{ÑÖ 4ž³ô-åŒ vTšQåhT¤Èô– Bò,+ªŸ÷ìÿO âÜ ¢2Hƒó¼}CTZ_Ä9å3² ½ûÿÓ}ÌŒ ^nܧÒѱoSv̆ÆÉgÕ·HÞjÓÿÖà«õA×¼—þ]úw l J£ùÍ=ÿÒê–úF*”–ßjëÕ£¼-òi Âó¤Òí½ûþé`AµöÅ8x±G@ Ç´G3‘”²6ȉ&o}{ÊÚ{_K½rƒ|MÎÁšZÊ/É'*X–¦^žòX‚íÌ,hDÿ€ ]ò 8bxë¾÷椷Î`V¯›Òès0G`†‡šȦ–,í¾d›ÀË<°&llÙl·»ËgµúÏRÞÝú ã À‚†Àç£ç×»3_Ú-=`ô|f_ß1èZ6ÙÝÛ6ݧ¹.bª]—ÚKÊ_[~ëÛ%^¹½ÒXÒóù\Ñx[µÜ­÷~{ ‚ žAÚ‰| Vƒgâ\b€Èµï}wynÿþ‰,»”wTzoÞɨ¼q<Ç{#=#+>;+÷寞 Ü»=ÑЫotÚ)hŒõèíŸ{]o•£G_O_µdã ¼ÅX¹/kÜReKÁFZx)ѱ‘®§§µ™)o±·±ÉÑÈB4ÆRžŒ¨S{‘3Œ–`PžþoíŸÁš,r¨õϨþ,ÅPˆu¼’ž9xÆÏùÖgÛòÌ¿õy‰&Vk¯oëà ã±äRYó×b£-‹‚ò·±¾_º—µ]"ÊãéȶUö¥¾¾ÕÀŒ råéÿR€¡Öç(5b½Ñ%­Ïut–ž¯–òhÚ17¡ðŒÏ­rëq~G‹1ðÖç%*˜XM>ß­ 7ÊÕžµGþQr«×ìÅÌÐŽRùkUwálÑkõlu¡Gõÿ¨|<†ó¨±¢e\ÒÈgäØbgž>¢[3±Ùñy‰M>Cƒ=å~áÔÃÊù”1åþ5{m¼Ÿ Ž´BŸY%xTd9GœÜê™íò¼x'|9ù|i+å ìb)䈾µã[”z‹ÌÊØÕeÊ¥)íÄê#®¼}h…×Íf fÕêiѼ:ÛÚ'Z€·ÞñªfìZ]èÖe—è jFÀ[ž—RP/«ü­ýä#†5».¡m“ÓªuŒÚX9k{yÛ©gÞQíåͧµ¼Ä€xßX÷}0ãó¢)W·åóO%Ö†@ŒKhE2ïÙ^=_Æ1ž—ùäOÐ!€ùB€!°ÿž#P;'Ùr3rJ~tÐ xаž>/2Pòïä+ù×å¬ôtü{² Vù³Yà=4-  üÖ†s6† C#n™Ýå³Zýg)ïný†ñ`A€åP ÏBRÜæ›{p4ûÁI£Ê§½tb¥öúõ÷`«Ü¿sù[”w­Þ¥þ¼ãÁ[¹:sÙ>m ‹yzÏ~räê8×G;{`–á“Ár˵ïÕrí~O_Sâ-eÑ XÖôÞþ¼“'åÊ7Â[ûýŒ2_¥œ€!>ø÷V.X»ÏÉm%Wk/k{zÚ?M¿’qÐÛ£¦5®fÞ\NØ“o̓[êÐ9w¿´`¹×Ýõ«Í_sØÑÝ-åï)sꦮ¹²KõÊ•S’KM5¹YËóô`eŒ0"g‘µþPj/Oÿ·öÏÖçÈ*‡Z-å±ÊMzÞ¥>UNkù#Æ1ï}#úç×LdHŠ£ô·gÖ“vkþÒÃW8¥Ö« 4ùäîk âTº—µ]"ÊÓ{Àˆ’}©oD§o˜-ò/¥÷ôÿ\_‰xŽR#Öò¬hú³ÕXôì=‘ú\ízk9¥qÙÚOJùhŸ%ϸjIO°» §Ü]éÀa1÷È?Jn%ãÉÒá[Ý×#gòµ£¤H"䯑kïôµz¶ºÐ£úT>ãíégsærYž‡^õÓôMÿiéÏð!ÐÃØ££>ýpk;î›-Ûýdá‰Øp•ô3gm—Èr2†Äó¥mÄZ0¢ˆAoÔko­ÊÌ’Ö+3ÏÒJÅ-íìönœ‹Úù¯12¤%$‹­é£ŒÝty¡¶ë¼Ö^‘}¤%Ÿ{ù´Þ•–óž~ŽFÞ÷I#ÀromX{M»côx]7šuûÚZ¤6i°Ì½šÖsöæ•›TÎ’ÕºI˺ÖÝs¦QÚ,eí-ƈµ¶¤(gÚæštRz‹¡bÍ'ÒË!õ ©?[ž œ%ãS;D”SSþèe3ïsUÎZÆ;¨¿>ÉB-ì.´nrZ±ŽQ+gm/o;õ>„+¢½¼ù´–÷‰M“ðÎ1–vhŒ¼vèÌ+[®XÞ{¶WÏWé‡@»/hÀ{ùÆÃ’ eÇÓK©Û´åà IyßÛ¢³!çÆ-ðYEÎð!°ºÒµ!K»aЂe'²§<)_o«Ð(eh}kà-õ€Í ôi¶›¦É‘; “O-)?My¼ž‚4ŸÚr¥^¥ëQžœ¥wºµrŽ’?øQ(´{PÒ8QgÑ[þŽŠ^Fð pÞT§÷YùÖý½ÊCð ׬r”B†g!xÀ»po|{P'ì ³f‚‡àPÏÞªó÷osïðçÎä ½mió'xÀ»¨*)¿·ÕY™,›2VV8;–9À£†¬Í"€ÕÐîç`"@ !àÙåo°{žš·’òB©Õå`9o¹#à8Žì@\‹¦1FÀ ×^h´îòN‹Òï7žYÎå)É¢tÚ!F€LõH…b=ߺۼ”>woéýéü)Íõ‘ T*{D½¤|J±¤v·Ê0¼JH ÎcÙ7` 4SðœÈ£x=ÆCkð%«wŽ|ëlµ4k¨•EJ¯ÍßZæ4¾Bn–’¦±–Ó’EnÖò<­˜¢Ê2ˆ(£¶?D÷ÿÑÏ‘Už±†= yTG {‚ùxN¢KgÉ_sH‘¶ü^¥³` þ£¹§&ÿR»D”Gkày£BFɾÔ7¢Ó·ÌV#ƒnyú®¯DHcchb%ðLmäNeÓ(ûœBÖìfï’{¿öw©®¹ç6çzõ0P©[Öè5§ ²ù^ïÐb RT 7½ãÛ’g¨7`#ÞäGêÏ€G ™=iƒY Ö*oíѼ%ÅÞ¬ÐjAv>·ï-Au´é[î{ õÒwé{ô£‚ùÜÔ\þ^¯†€03ŠLK º´d ùw(¥zý;w€Ž6È&羚 >gä Fkðk0kÍ’á»°½!ÐË()üHº¸ùs/cž…´%¥«Mu_«·£#R  Üf½Ô{àí’áÐ’àõ†ÀLX•{ú6B—õÉm/m¼‹ ò‘Ïd§ÖÖíQØv– :ém¸>©÷¡ô›b¹¬Qý¢mtpE”¿§7N¦.ûÔ; ¥¯]À#0@±ßƒI×KÁ…,›»ïn/­·×‚äB>…ÐrßR^©‘t¬¯är¯ÍökéKùä6z‚¼Žv5îÙðjÁ~&ô†Àº¼À xrÀñÅÈെ@ï C–¾@È«,Òß•I¿º>Lèü<IÉ“Çtð8 €èAòR¸¹7¢Ž¾ßcŠYho];Òx–r*ukúû©„µë[Ö C­3ùk~Ÿ‘G*pmÌ‚×ì–Œ'ð6¯ÀÕ¿-×VeXСÒLÞDHcdt/ Í´ïJÖ\(7s×Îèk÷µ”ßZNkÙJ÷m˜ùt`!t¨$Èb HùHA‡Òÿ7ÍôD9z‚ ÕwT"?3K¢OCù݈ CV@ëV¿§)E¬y4÷ sõŸJ#¡IÐÆ{‚‘R24,õò–?õœíý CÖÙ¿Õ0°ðÈú}Ô¼(!:HQïzèW‚øvŽ€6ÈOmVßcP2&zì  :¤10fÙ ~ËPn­¡HÐ!€…<å*½.¤ŠB›O—mný\“>—‚ÿXòñ–ÿ~­T¯\9?r0z:ЮƠÃl/†L ±0 'Ñ¡f寥ßÏçcZž5þÀ{qî¡ô»Z^µÓ-i×±«é<‡óDðÔ}{•7¨>³ìOàUHŰ·¬G»J'¦ƒ´f—;ePʧtÒ! 2"V3R&WúQùcœ¼“aKÞ E­³7M¢^Ê#b AA”¦‰D ðV€%H‘GñÞôhe¦ jA…îÊ­%°”¾å¾š Cˆ4åÍýö¨”Mº×áëÓwC7íç÷ë¥ô÷ÿ§Ï„%ŸÜ}KùKç/xÊ *@éßµ™YΕ_rá[bx¼n£@£T¯{‚iÒ·Ü7:8ÒGa$|*å)½«‘ÃÑÞ§ï†mîoOp$K>ÒßRþR>QAœC@mhgêV7»5À‘´`È©„¥ÙymÆjMußOP9Ï ü£Ò;=ž>î}V4÷Õ”!—OT'ÀèÊ«×Þ%7¸¢·wСÞõzB¾g»7 7sžÍ˜öæC'€½xí9×L^ R$Íô#=¡AŠ\Ù]”pïàHg‡ò^2j™ ʇ>|øðiýü»4pÇçýäð 4pŸÙÿüä=žôxêü»4p)ÿôÿ=ÿhE--X–îFÒe8¥ßi®—òxÌHR/#àR¸¹5þtFo]óÏ¥Oïsý[º.!Gé5ɺþÝÓÈp= Àª¨s³ý»1P3Z¸ÏüSÃáúÅKoD2 ¢–'$%_À€vŽ@iSßõvÖoI¯-‡Õ;óܯ§ûjéž =Ö€!»»¶>»|ÜKÇqükñ÷˜UÜg¨×Ì>GnÛkÖþ¸7 #%OåÂzÉìòézÜŸÙû³ ûñÝ:¤ÿ÷*Ø’bPàÝòÏŸ‡ðÝ}!&ý[JÿùÈ˧ᾥë]ÓÖëWó½ÿiò‘–6JùäʹD"åo©WoÃÂ*Ÿýij†@úàÕÄtÆy_Û¿+gͬ»¤Øs¿Ïåßu| JøLÒ™ëRú#óûZzÍõ ñ35%±v½Öçr3×ûoµùÔ~«)§¥üwïYNákËãi‹^o;Zå٠ذZâ%…oUÈVÃA›ÏPa¶~V~s:ï“™9wôqMƒx”¸ç#Í<[ò¶*+©%Gë=sés3üZ?ÁÃtç\3ó»ë¾‡‚÷xßPw:s Þ€s¼•ºÈÓYß(…y}¬Êä^Ζ|F¹= =6ûFxRkú Þ€<Òéúý»Ü5‹7ÁzßæÙöõïÚ:ÿ'ó]šþHfðµü?Žë Š\Úyî½®½÷¨¥„R9#”§Ge_’[ë&ßZùs÷ÕôéïÖþeõFo›^#|ók‘Ö™óè|V«7`` XCÞA‡0nÔvÅ©àņ`d1†(€7°©!€7`SC#`slhà ØØ€×ÂÃGC06åÍF`™IDAT—ø Àx›âÞ#ðûûùüüÔ;ò•&×¹Óß§yzò)•ËZ çM =Ò·þ.ê÷ÑŒ–Û'ùý©x@r÷= iß(·£ Ãûy­“wt˸tžÿÕì8Ž¿®ß¯ÍÆyž•Ù[Þê«)ÏrÞûÂýÒõ‹ïÖlN1ç”~t>Wš(å{ZÆ%rþ?Í‘Q’çDVo=¦©oNnKÎzfn—‚"=Žã/°rÝjß­RßÞå,õI~, äuÍ0È)ö{>÷¿/c`”7àsS½ÒGÍ’ŽÏ\Êjv¹™ûžÈmY¹­Ò`Ú1¬¤^-ÎMê)Íô£ù–n¦tÎußb̲6vfǧì&> ùäf%9׳f.¹¦5npï}W—Û̳í™åv ”Ãa”TŸ.eŸñÁkP*¹¥„ûõk昺³%ODÍm-cÎnu­[ôÚ=ï\krÓxj¼íÒ²l¢6<—\÷%…n™…— ^îÉzÿ>uçŸÒs^rQæò×ÎÎÎÌÄ£VÎÚ}ß*·S1Î?¡üVèož‚’Áq䣕çÑ»Ÿzð Öê:–þÖÌÖ½Ê+§ˆÓïZ뫹wúÿœáS3 "ÚÅ"G‹¡—õD!mø³-›{zøz*‰ˆ÷ ºï›åV›ÄEÈôrk¼jä[2t>Ÿ.Þ‚§¼Š¢Ð\_Ù Ÿz3fß8©1®4u¶Wß#+*)ó܆?¯ñ0MLf/£ºŸfw;ró×çÆr{²M$£D2 +`â/§¢f¶3×·§k½W»ô2ŒÒ{…o´®ó_Ë ÷~^/Äuo­¡Ðs_Bn³ÔèÁ÷ jÏ‘{7fÛ¹‰K)¥½´>£5U{1åñŒ…V.­o·äqžç¿Ÿ(åÖârÏyf™é[eí%ùË#~éÝ,˜*×Å®Í'âÕÁ£2Ó¨=÷é²àY™U¶. J{μÞj½¯Fž«É­u ?Jòr+mÜKó·ÌÐ¥~[ªWi?„G¦qcăçÔk›Ì¤ô© ½¦tJé­nlé·¹MŠR½¬õµÎÎ5rÓÊÇ[ÎÞƒs÷Ïïo|žçÿ?Ò¿w• r{§ÜVl§'Ûk¥ò˜úÁ?ÚŒÏb2%úàwíça75rCn£Ü໽ãÿ06† CÐdý¿ù~°œÒu_« PÚœÛ +]_ýìt€×o”Q8~™å»dH\Ÿt'mzð … /1Þ€èÍC±¸ƒi‚Bhó©½/z¿^J_*§'ˆD)¨†F¹YË‘¾V‡R}[  ´?Ügö°€!` Î U躔S|– Ò¡iY5A6¤ü¢‚oH[ë‘ÞÓ^µþ` ì!µž€Á†À“Á¬÷Š:uÉêbŽ’IzzV­<šûJyÖêÕî’G@cüõ0LÀhÜȃïlA6F”§•ªµÝ­˜ƒ¯œâñlÈZy×leRaÁ[ÚýÉ {<žµÙÜ&¶¨5ÞèµâR MPˆV…nY3·ý°æß£½Òt¹3ÞA78QÊK¿a<XÞ#ÐóèXéT¾›Ãz­'ϾN=¢|–Wsƒ,×¥ûiÏм–Y;ïÀúŒHýy·W+KmÊ~€‰ ž )·Þ{€ ‰ß-·\û¦JQ:a1ºì%#4"ý›DêiL—&0–nA‡FÌJß8Û©”#¼++2O(åÖS0W šÔÛ£F, €E û:_)¦|íše¦Î´ù[âÛ—ò·–9w8ŠWS¯Ö|,r³–çiÅU–F@Dµý!ºÿ~Žà9TKžà9ž“îrûhó—Ö‚­ÁyZ”v ØD¨vOMþ¥v‰(ÖÀk‰’!ûR߈Nß2«¶Ê¿”ÞÓÿs}%â9ºçËòÀB†€Æ@è5Œ¸—椂,•Œ'Ë Ä©ÇL¾,J³G Ê+`5pz§¯Õ³Õ…Õÿ£ò!.À‹ hz O4ZÅõæ‘Aßßo[½1Á¬f Òq|iëÓk µq¨e³ÌF¯´^™=ôé^þ¨×¿¢Öø¥ xÚ·T,r´¦2v5Á riJkõ‘û+Z~ËÒÀ‚ïyšuûÚZ¤6i°Ħäöö(É´^9e— úäiM¢ Ò+dÖþÐbŒXûgKúˆr–‚SåÊf fUZ±äÓÓËÁúës‹5Tx`}³ºÕß'×n²|K{yÛ©gÞQíåÍgdôM«÷1  “!0zðÚÁ`Xyà"ðží5ÓA?ð^¾†lÈÿvEhwf\IEND®B`‚sugarjar-2.0.1/images/subfeature-smartlog.png000066400000000000000000000106241501047226300213010ustar00rootroot00000000000000‰PNG  IHDRmÓx jsRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé -²™êIDATxÚíQrä( @q*7ÊýÐgb?v¼ã%$ì÷¦\“84`‘4:B1À+ùBðvC Æøß2ȇvzr}ßÖ¿÷àíá²G ÆŽãP¿<Ú´é“~îúZóô¬ŸG¾£Ê½ëyw¯¯¶œT œŸÑÞ?ÿ––wþ.Ý/ýžËߢ¼kÏ]zïvëO£úÊJrÆOWÀèYE®Ÿ/Úyi­r¬÷}f€-ùßѾ¹~xöÙ\ÿ¼¦¯)ñžºh~kúÖ÷îMý÷Ì×Ë[ЛlgŒî´ÌºŸ—œwPrÞ^¦–~š¦ßÉ8íùÓW(rØ•o«+¹õs3¢Ö¥ƒë‹hÉ_¸r.ÐRþ^–¾T®$Ok==ž÷:ëi)7M/µ_­Íjík­ÏÝŠÉËhœaxÎtký¡Ô^-冀Žt¯×–Z´å®ÖŸC@K?·Ì&ÒÕš¿´ÆZ¤«uàÕ”k]CÎýìõ¼%¥¥-·$7kþ¥þãQÏÜ2`_ëjÝ·0:}±Êßú^ÔÞÓ\_ñxßS#V³ß¢$í˜èÙŸ\ »\]é‹ä=c÷ÊßZnO=gæ#¥/=—eàòªÏŒ™¼d°h‰Gÿ×ÈutúÚsöºÐ½ÞS¯|,Æ›—<å 0Å¡8Gtü»^¦»Ý÷gÌl3famý¡×ã±áÒ+Ÿ»äà™€–/íËu^£:æ,ë·g`è™å¥K5yz `^^‰»ÒkÛDÚÙݺqÎkç¿ÆÈ–º,r´¦÷2ÊÓå© ´íugßÏyZYhä`­óîßæ€‡yZ]ÓšuìÚŸ6é¥É}åk„"²”+¹ž[Öœ=ž×²‰JÊ¿äFµnÒ²®uœ–6;ZûmO¿²¾G=é=ê™¶y­n¥ôCŚ׬\zF«!jmG—ñ&$ ážò›-yêòôغC¿ùm‚‘róÚ çÙÎ#' ;÷Gx©!€âZÛˆ€ù9¬Û^»œ´Ç‰€°½!Ï…èƒ!„Ïý ÞÉ/€Åu}Ø~~äÁîü[-¯kºR>¹rkùiË §Ûò”Am³Qš®ô¹× ö9D⥥grW¹£êëô<Ýíéõ¸,Q@2ÄÅøïBÖ¯ÿ¥ëó)ÿ^»ŸKSJ+¥é)7›îï÷†þÿ{z?÷é³¹| —öYòuûó/L¾zÊ]ç\þµ2êÔÕ–Wî]Ž}Q›ÿùpqqÙ®¯¿“ÕÃýë)šÙÏ5ÍÏOÞ0jå’ïÕ õ"‘ä“}gm Ïã;uZÏC·*Þë€è­¬Ÿ<Àž D% É3Ê- ?÷¤ï)7 ÏÑSÏ\M}sŸ=*u“Êjx•z‚PÕ‚J¥Ÿi Ú£=g¡·ž0ÁÈvf™Ù¤ *·æ_ú½¤Ø[¼ÝF4«¿Þw¬4{+TÆ€F©†‹Sèú÷#s¿%}O¹G(¯Ç[ëFB¨Ô§ôÝ»Io*ï R– U¥h”ÖzÀ$C@kô(ö’’«¥Iá^ÉÍ/ɧs9 iccõ2 ,Ò– Ë ×£ÜàTÏ蔿WúF@Ë;j14¬å¶ÿ!¨À†€×ìv…ÑS´?À]¦’< J­ææï-×Mþƒó·_Þ€‚çŒÊ‡“öÖâ¶sΙüÕÕUvÒLßÓ°ËÆ­f#À¢;\ÙC”p c•zPßS†‡O[zw•J÷¤Þ‘Ï ƒ<µ=–ô¹µo)½öþ8ÅíéÒ3F¥õvi]=÷»&½W¹šYuT¦½*á«—ãú³%ÿÔ°‰9hŸW˜5[‚çÔ‚\õ•²Ôµ”W=ÀŽ/]ÊÈÍ`Á_ž—ß_ÝÏC0^'Ó€lC† C“Þ8éà½4ª€“[µ@TKï©—^·}Y0¸GÕt •A¥`AÖôž×A`ÂæÁp2ÃÅÅÅEÐ!ÕÌRó•>múeƒ­àÓ!4Òtèz0Ð,¤%—¥)`ŒG ›Ã!È ÀC kСtF]›][ ’»ã 7öÙ8È¥yA0‚Ì<Ô°-3îE-¥Ÿqü°]CˆƒáÔ¼9Ãáü۸!°’1myÂz†ài¨8éy J÷ñ¬Gó9¥M}¹ B%¥nI¯­‡Æ;`Ú —;³~´±±x0É p½O€µé:YÐó\KzÏÍ‚¿Ê-œ_œÙ—6때‰Ô°¨­ç×ò õi0z7 –ò€M § R‚ZaU†NÊ4lf<¥þ€!~t¨§Àlj‚ïå—GàB°,[¾æ²™/È ¥äQŸ© Ú› }®tw?›üŒÍ7psdiS¦Çû»Ò³€¿jù|Æ0¦5H‘&ÈMO0¢Üg{ëVRäd(8š8hç@E#å–æ§É_ûîKqq½#èÙ•à¤H3Cµ#5kòîú]‚=evy÷s\ßÙÒùð|šOl R$)Ø’b÷PàÃò/ò©}Ï_¤(„ö³ ´÷»¦[Îкš¯ýO“´´Qʧ4ÉC±Kù[žk´aa•ÏŒ~Ò²ކ€5H‘t¨%õ`¢\þCgÀš CÖ EGæóµôšûNã§5èt¿Öçr3×ëgµùÔ>«©§¥þR´ÅRþÖç²È²·[ÛÑ*OÏþΆ€Õ/)|«BöŠ2(J8…C˜­k#ZËI‚›ÆÌœ;º7‘—¸æ#Í<{ò¶*+©>wžò±3]ê',g,bxàå®ÄþÈ Eƒ¼½ŠeõÙÜ.3Í»äh-3—>7ïõ<>,wŽÀ93ï F¤-Çj°Üº¡.6~& Þ€8߈²#U‡ó²*“k={ò™mäö4ŒØìëáHe¬é'x6ðH{¤û׿åîY¼ Ör»gÛçïµuþù[šþHfðµüCÃýE.íp,ÞÑ-ãRŒŸì8Ž_÷¯÷V#Æø«Î­õ]áy5õYÏk_¸–!Ý?ùaÍæsNé{çs¦ñRîðN˸¤@âŸ4GFIÆ… ¬ÑzLó¼9¹m9ëY¹] Šô8Ž_ `çg«ým—ç]ÏRäwËÒ@NQ× ƒœb¿æsýù4fyÂE9ŒJï5K:ÂZÊju¹ÅL¹¹m+·]L;†•”À£Åù’ç”fúÞ|K…isÝ÷«¬ÅÂLâe7ñQÈ'7+ɹž5pÉ5­qƒ·–»»ÜVžm¯,·c¢£|¤çR÷_¼¥’[J¸Þ?gŽ©;[òDÔ\ÐÖ:æÜèV׺E¯]óÎ=cMnOMk»ô,›¨ –ŠK®û’B·ÌÂK†Æ(÷¿ä½þ=uçŸÒ{^rQæò×ÎÎbfâQ«g­Ü§Ê-*Æù;”ßýÍÃSP28¢A>Zy£ñ®¯AÁZ]ÇÒÏšÙz«òÊ)âôo½Ï«);ý?gøÔŒv±ÈÑbèe=^Hþ¬†AÏ&À‘¾‘JÂcàNå>YnµIœ‡LŸ(·ÞÉ«F¾%C' |†x î~ñ*ŠBsg7|êÍX}ã¤Æ¸Ò<³Õ¸úžù ’2Ïmøk5–é€ÉìeV÷ÓìnGníãu|±ÜîlÉ(‘ ˆÛ¿°ð‹—S^3Û•Ÿw¤k}T»Œ2ŒÒ²Ü7 Z×ùÏå„ëF¿V/ÄY¶ÖP¹/!·YjöàÚsæÞ䛸”RšÑKKá3à˜ ^|¼Þ›‡0|i:¤ ¡Í§ö}ÑëýRúR=[‚H”‚jh”›µþékÏPzÞ(í×™=l`Xƒ3HG¶F]Ê)>K éP‡´®š R~^Á7¤Ÿ­Ï둾¥½jýÁØCj+<3“ ;ƒ3XËò:uÉêbö’IzzV­>šr¥J¹ÕhœÝ¾¹~˜*Eé$HﺗŒeôO:¸i¤Ñ_šÀ<…aA‡fÌö˜u?–æåØY‘µ„|î=­s§àN£=ÄÌ€×{$×hú÷ZЋ~}-ùK—Gð"‹Œjå–f¡–zz<¯4ëi Ž$µ_­Íz‚>­vk}†à5sÖô‡R{µ¼§£‚•`,nm𜖓îrûhó—ÖX­ÁyzjM¹Ö5dm¡–ç-)-k°#«R´öúXšUáhä`Ý·0:}b´Êßú^ÔÞÓ\_ñxßS#Ã^oÌtÉIù˜±{åo-·§ž3ó±/jQÄ^õ™1“·ìh5†µø]ékÏÙëB÷zO½ò!~`ÜÀ¨—í®ø.E÷Äý3ÛŒA¿­?ôzc<‚n­L `¾´/×y gmÈézfyé’GMž^ØèàQw§:ÛDúxëÆ9zJð´ß¦±ÈÑšÞË(×­Ê¥)}oÿ®¾ŸóFNP¶ò´º¦5ëØµ5>mþÒ 4:ˆMK¹’ë¹'ˆPÏóZ6QÕ‚å”u“–u9,mv´öÛž~e}zÒ{Ô³DKÚj ºUZ±ä3Ò˰ ÿ‹5 Ùý ¾.Pðm  ;ô«‘ß&)7¯ÍžíìµiÒcp»!€âZÛˆ€q9ì×^+}CmÀsùBðBþ¶‚cé°í OIEND®B`‚sugarjar-2.0.1/images/subfeature.png000066400000000000000000000042731501047226300174560ustar00rootroot00000000000000‰PNG  IHDR.Z´q¨sRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs°°é~œ½tIMEé Ë"|uIDATxÚí]aÎã* L«½Q©ïÇ*«<„Á6c0#UÛ¯Kcc ÅŸã8~AA[âKA‚ ‚ v~¿ß¿!cwùÌöüoáw7»¡?!ˆ9ð9g~¿ßñù|Ôƒ\Û6u„é÷žŽÂJÉßHº³ñ§í'îïh?¿ÿ/íïþ[ú¼ôw޾eò®=wÉžßn?½leöq$ù²t³r¿»ðÛ”ˆ^ýäy ø~iW\eød0Bn9ýÞ¶Óû³}moáE3°­í½ö¼S&妋Êô–ñSÇ-}Ó‡½Sn³øÕ)èè‰QìX¹Í0ÉiVuyxôŸ¶Ÿ)8ˆÎ¨iƒ«7ú‚7ôM¬=Vzâ5Ò‘Òú¹•›wëàé0,ô%›KÕ–è{xNÓÔµTvé¹r|Jr©É¢&7+?£*ë W&5{(éËcÿVûŒL•×¶Z´ý¢äiåSê7Ç?ÊoxV¯%>v…ðÏ5¹Ñ¯Ò–Þ{V=©à¬ô¥½àÚ@— Ã;Aièäú­ím[&E«^ü ¹e Xe_² tûÖÓ"ÿR{ýçl1ŽRgk+Z>ò¬9ví.Ññµˆqêñ“Ö³9Z>=þÙê÷Vö«ÝQétÀ#VôQr+94‹Á´¦¯{®ä¥¥™Hò÷NÈöµçlM¡£ìEǼ¡ä€¦#MÖqð‡(úžÅŸ èìæW_D¤#„7Z!Ú¶ò~#2­¿ Ù*ͯ&zÒ%$Ëøí!7也úÍÿÏ€¯Ö ܯ(¡¢¡£^ïjônë•*:o}Véd·÷àêä¿&ȶ,r´¶G»éö‚¤­¾F®s¾D+ ¬KiTë!-ë^wäJ¦t(Çj-ÁˆÕ>[Ú#øLu^ã­ÔÞ¨Xé VåÒ3ZQ<=gU¬úÊýœÖ"g«½yüdÍ¢úÕÐjÕËj~ÕíäB¡^i´™8ä4Û3¢V¾U_^=E_…З—N+¿‘øÌö¸š¯#úÓÐ@€†€Wzt&‚üh}ÍrkÞ*·û1à8}} @Aĺ`õA‚ ‚` ðo®f©A0ã½Ï£xnî÷$°k2_{ØÕ¶÷ç_AÌô(–Ò£X‹ÃöõBžê— 4'X\‡ :⤨_‚UÑ!kñK‘ -ÖLA+Ÿ½‹´Ó°òc:})>?…¶§¢ýùø¿³¡ß+ù×ÚoÚþ¾ @‰~‰^ý·~îæâEO¹\J½,ª_‚€Ì“Çáïw¿¾n¤íÒö¥ï—ÚHt´ôS>%~#ùl‘—¾ô}+?Zúÿ^—ð7òó+Nî;È~­ôK_Æ~ vU²ÄçQv.Ê£—]½L¿|ñ…x™o´–•´®Z¶V-b¥o½Ý ¶rÚŸƒèœŽçº ý øA­*7½¥×~GŸ ßÎÛL¿Û°\O‘E'f*"9ñ×]U9Êž/{®Ò€û)•&ž ×¾ú% ¾¨U:¢ÈF möb (ÚáÕ¼¿*««+ ßÛk&­§<Áö£-òóÚ_Öœ [ÛL¿¡Î´‘@Ù(}Þ²*Ÿ¡ˆ´*ÓÐâ§è¨jŸŸ•W©=ª_ -k¿gòù™™t,ôŸ“Éeàß0Yì_;½ã^ìl\á/ _‚hž'^1LDg˜z“'å»–|¨_‚Á@€P¯0ÊvIùP¿‚ ˆ¹Ðò ‚ij Ì0è êi~w³oú=bwüñF°ž:ÕÒ­aõ™£êh×莪ß=[Ýðü–î–¨M~éM“µÏ¥þÒ(¥ç×ü´fwÖ±,;‹ÜV@I§;É` ÐÅÑçN×â*ßΓ²7hì­ßœ¦“âó½Ä‚÷R°Œhïw;ÙoéZt‚X ßNuFú >Þ±JCefžÈ¬vç±SMоº­H2LoX$ˆí2Rj4ýÿ\ZÓ»uðˆúšËOÜEN 2ªõ[Z…ZøDª}í9[Sè¨qŠ¢ã Þb»@ bâŒl£ð¨‰nÅó=uF§ï³‡Öl ¢ˆÖ+‹qÄËñÕ®ûå {Èiq -«¼tË£&O”‹. =¢ätN'ÒïÀ½çPu.4A†´Õe‘£µ=*(×)ʵ)ýn”íç|^ä… ¦ÊxSÓš}ìÚŸ–¾ä„àEN”“F©_)õÜRô©åy-‡¨jÅgr“õ–ÕG®K‡­vÛbWÖqÔÒÁ§¦Q*WK­ÒŽ…Nd–ƒ fÁÿ®Öœþ%°)P« ÄÐì*òבrCîDêuh±ˆ"ˆá'®wDœ#'æÓ×›~ È@€X: ‚ b]|)‚ ‚` @A‚ ‚ A±þöŒ…O¯+ IEND®B`‚sugarjar-2.0.1/lib/000077500000000000000000000000001501047226300140765ustar00rootroot00000000000000sugarjar-2.0.1/lib/sugarjar/000077500000000000000000000000001501047226300157145ustar00rootroot00000000000000sugarjar-2.0.1/lib/sugarjar/commands.rb000066400000000000000000000211111501047226300200360ustar00rootroot00000000000000require '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 = {} @ghuser = @repo_config['github_user'] || options['github_user'] @ghhost = @repo_config['github_host'] || options['github_host'] die("No 'gh' found, please install 'gh'") unless gh_avail? # Tell the 'gh' cli where to talk to, if not github.com ENV['GH_HOST'] = @ghhost if @ghhost return if options['no_change'] set_commit_template if @repo_config['commit_template'] end private def forked_repo(repo, username) repo = if repo.start_with?('http', 'git@') File.basename(repo) else "#{File.basename(repo)}.git" end "git@#{@ghhost || 'github.com'}:#{username}/#{repo}" 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, ti's a shortname cr = "git@#{@ghhost || 'github.com'}:#{repo}.git" SugarJar::Log.debug("canonicalized #{repo} to #{cr}") cr 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 determine_main_branch(branches) branches.include?('main') ? 'main' : 'master' 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| next if line.start_with?('(HEAD detached') 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(fallback: true) branch = nil s = git_nofail( 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}' ) 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 = git('remote', 'get-url', 'origin').stdout.strip 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 gh_avail? !!SugarJar::Util.which_nofail('gh') 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 die(msg) SugarJar::Log.fatal(msg) exit(1) 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 git(*) SugarJar::Util.git(*, :color => @color) end def git_nofail(*) SugarJar::Util.git_nofail(*, :color => @color) end def ghcli(*) SugarJar::Util.ghcli(*) end def ghcli_nofail(*) SugarJar::Util.ghcli_nofail(*) end end end sugarjar-2.0.1/lib/sugarjar/commands/000077500000000000000000000000001501047226300175155ustar00rootroot00000000000000sugarjar-2.0.1/lib/sugarjar/commands/amend.rb000066400000000000000000000006361501047226300211330ustar00rootroot00000000000000require_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-2.0.1/lib/sugarjar/commands/bclean.rb000066400000000000000000000065771501047226300213050ustar00rootroot00000000000000class SugarJar class Commands def bclean(name = nil) assert_in_repo! name ||= current_branch name = fprefix(name) 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 def bcleanall assert_in_repo! curr = current_branch all_local_branches.each do |branch| if MAIN_BRANCHES.include?(branch) SugarJar::Log.debug("Skipping #{branch}") 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 private def clean_branch(name) die("Cannot remove #{name} branch") if MAIN_BRANCHES.include?(name) SugarJar::Log.debug('Fetch relevant remote...') fetch_upstream return false unless safe_to_clean(name) SugarJar::Log.debug('branch deemed safe to delete...') checkout_main_branch git('branch', '-D', name) rebase true end 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. out = git( 'cherry', '-v', tracked_branch, 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_branch) s = git_nofail('merge', '--squash', branch) if s.error? cleanup_tmp_branch(tmpbranch, branch) 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) 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) git('reset', '--hard', tracked_branch) git('checkout', backto) git('branch', '-D', tmp) end end end sugarjar-2.0.1/lib/sugarjar/commands/branch.rb000066400000000000000000000021571501047226300213040ustar00rootroot00000000000000class 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-2.0.1/lib/sugarjar/commands/checks.rb000066400000000000000000000104611501047226300213040ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def lint assert_in_repo! 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') end def unit assert_in_repo! exit(1) unless run_check('unit') 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 def run_check(type) 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}") 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 $stdout.print( "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " + "changes to the current commit and re-run\n > ", ) 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 end end 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-2.0.1/lib/sugarjar/commands/debuginfo.rb000066400000000000000000000005531501047226300220070ustar00rootroot00000000000000require 'json' class SugarJar class Commands def debuginfo(*args) puts "sugarjar version #{SugarJar::VERSION}" puts ghcli('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-2.0.1/lib/sugarjar/commands/feature.rb000066400000000000000000000020151501047226300214730ustar00rootroot00000000000000class 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) if base fbase = fprefix(base) base = fbase if all_local_branches.include?(fbase) else 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 def subfeature(name) assert_in_repo! SugarJar::Log.debug("Subfature: #{name}") feature(name, current_branch) end alias sf subfeature end end sugarjar-2.0.1/lib/sugarjar/commands/pullsuggestions.rb000066400000000000000000000022631501047226300233140ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def pullsuggestions assert_in_repo! if 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 not going to push. Please commit ' + 'or amend first.', ) exit(1) end end 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-2.0.1/lib/sugarjar/commands/push.rb000066400000000000000000000031651501047226300210260ustar00rootroot00000000000000class 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 if 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 not going to push. Please commit ' + 'or amend first.', ) exit(1) end end 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 def run_prepush @repo_config['on_push']&.each do |item| SugarJar::Log.debug("Running on_push check type #{item}") unless run_check(item) SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.") return false end end true end end end sugarjar-2.0.1/lib/sugarjar/commands/smartclone.rb000066400000000000000000000017051501047226300222140ustar00rootroot00000000000000class SugarJar class Commands def smartclone(repo, dir = nil, *) reponame = File.basename(repo, '.git') 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 == @ghuser git('clone', canonicalize_repo(repo), dir, *) else ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *) # 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 end end sugarjar-2.0.1/lib/sugarjar/commands/smartpullrequest.rb000066400000000000000000000070361501047226300235040ustar00rootroot00000000000000require_relative '../util' class SugarJar class Commands def smartpullrequest(*args) assert_in_repo! assert_common_main_branch! 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 args.unshift('--head', "#{push_org}:#{curr}") SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}") gh = SugarJar::Util.which('gh') system(gh, 'pr', 'create', *args) end alias spr smartpullrequest alias smartpr smartpullrequest private 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') return if 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-2.0.1/lib/sugarjar/commands/up.rb000066400000000000000000000066421501047226300204760ustar00rootroot00000000000000class 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 private def rebase 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 brach 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}" 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-2.0.1/lib/sugarjar/config.rb000066400000000000000000000032321501047226300175060ustar00rootroot00000000000000require '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'), '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) # 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( "Not warning about deprecated option '#{opt}' in #{fname} due to " + '"ignore_deprecated_options" in that file.', ) next end SugarJar::Log.warn( "Config file #{fname} contains deprecated option #{opt}. You can " + 'suppress this warning with ignore_deprecated_options.', ) end end end end sugarjar-2.0.1/lib/sugarjar/log.rb000066400000000000000000000007151501047226300170250ustar00rootroot00000000000000require 'mixlib/log' 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 sugarjar-2.0.1/lib/sugarjar/repoconfig.rb000066400000000000000000000027431501047226300204020ustar00rootroot00000000000000require_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-2.0.1/lib/sugarjar/util.rb000066400000000000000000000045171501047226300172250ustar00rootroot00000000000000require '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(*args) SugarJar::Log.trace("Running: gh #{args.join(' ')}") gh = which('gh') s = Mixlib::ShellOut.new([gh] + args).run_command if s.error? && s.stderr.include?('gh auth') SugarJar::Log.info( 'gh was run but no github token exists. Will run "gh auth login" ' + "to force\ngh to authenticate...", ) unless system(gh, '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.ghcli(*) s = ghcli_nofail(*) s.error! 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-2.0.1/lib/sugarjar/version.rb000066400000000000000000000000561501047226300177270ustar00rootroot00000000000000class SugarJar VERSION = '2.0.1'.freeze end sugarjar-2.0.1/packaging/000077500000000000000000000000001501047226300152545ustar00rootroot00000000000000sugarjar-2.0.1/packaging/README-brew.md000066400000000000000000000012301501047226300174640ustar00rootroot00000000000000# Homebrew Packaging Notes ## Prep PR * 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-2.0.1/packaging/README-fedora.md000066400000000000000000000046351501047226300200010ustar00rootroot00000000000000# 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 Start the vagrant machine, ssh to it (`vagrant up; vagrant ssh`) *NOTE*: When provisioning the node you have to do: ```shell SSH_AUTH_SOCK= vagrant up ``` Since part of provisioning uses an RSA key that agents will refuse to use to sign data. 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-2.0.1/packaging/Vagrantfile000066400000000000000000000013011501047226300174340ustar00rootroot00000000000000Vagrant.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.ssh.insert_key = false end end sugarjar-2.0.1/packaging/provision.sh000077500000000000000000000010641501047226300176440ustar00rootroot00000000000000#!/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-2.0.1/rubygem-sugarjar.spec000066400000000000000000000045171501047226300175010ustar00rootroot00000000000000# 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-2.0.1/scripts/000077500000000000000000000000001501047226300150175ustar00rootroot00000000000000sugarjar-2.0.1/scripts/get_linters000077500000000000000000000000741501047226300172650ustar00rootroot00000000000000echo "scripts/run_rubocop.sh -D -a scripts/run_mdl.sh -g ." sugarjar-2.0.1/scripts/lint000077500000000000000000000002201501047226300157050ustar00rootroot00000000000000#!/bin/bash scripts/run_rubocop.sh -D "$@" || { echo "Rubocop failed"; exit 1; } scripts/run_mdl.sh "$@" || { echo "Rubocop failed"; exit 1; } sugarjar-2.0.1/scripts/run_mdl.sh000077500000000000000000000004431501047226300170170ustar00rootroot00000000000000#!/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-2.0.1/scripts/run_rspec.sh000077500000000000000000000005011501047226300173520ustar00rootroot00000000000000#!/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-2.0.1/scripts/run_rubocop.sh000077500000000000000000000005141501047226300177130ustar00rootroot00000000000000#!/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-2.0.1/scripts/unit000077500000000000000000000001131501047226300157170ustar00rootroot00000000000000#!/bin/bash scripts/run_rspec.sh "$@" || { echo "rspec failed"; exit 1; } sugarjar-2.0.1/spec/000077500000000000000000000000001501047226300142625ustar00rootroot00000000000000sugarjar-2.0.1/spec/commands/000077500000000000000000000000001501047226300160635ustar00rootroot00000000000000sugarjar-2.0.1/spec/commands/amend_spec.rb000066400000000000000000000031471501047226300205130ustar00rootroot00000000000000require_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-2.0.1/spec/commands/bclean_spec.rb000066400000000000000000000072541501047226300206560ustar00rootroot00000000000000require_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).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).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).at_least(2).times. 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) 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).at_least(2).times. 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) 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).at_least(2).times. 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) expect(sj.send(:safe_to_clean, branch)).to eq(true) end end end sugarjar-2.0.1/spec/commands/branch_spec.rb000066400000000000000000000032051501047226300206570ustar00rootroot00000000000000require_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-2.0.1/spec/commands/checks_spec.rb000066400000000000000000000153031501047226300206640ustar00rootroot00000000000000require_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') 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') 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)).to eq(false) end end end end sugarjar-2.0.1/spec/commands/feature_spec.rb000066400000000000000000000035501501047226300210600ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true }) end context '#feature' 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 'creates a branch based on requested local branch with args' do branch = 'foo' base = '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', 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 end end sugarjar-2.0.1/spec/commands/smartclone_spec.rb000066400000000000000000000032001501047226300215640ustar00rootroot00000000000000require_relative '../../lib/sugarjar/commands' describe 'SugarJar::Commands' do let(:sj) do SugarJar::Commands.new({ 'no_change' => true, 'github_user' => 'myuser' }) end context '#smartclone' do it 'uses git if the repo is in our own org' do repo = 'git@github.com:myuser/repo.git' expect(sj).to_not receive(:ghcli) expect(sj).to receive(:git).with('clone', repo, 'repo') sj.smartclone(repo) end it 'uses gh if the repo is not in our own org and sets upstream' do repo = 'git@github.com:somethingelse/repo.git' expect(sj).to receive(:ghcli).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 git clone' do repo = 'git@github.com:myuser/repo.git' expect(sj).to_not receive(:ghcli) expect(sj).to receive(:git).with('clone', repo, 'somedir', '--something') sj.smartclone(repo, 'somedir', '--something') end it 'passes additional arguments to gh repo fork' do repo = 'git@github.com:somethingelse/repo.git' expect(sj).to receive(:ghcli).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 end sugarjar-2.0.1/spec/commands/up_spec.rb000066400000000000000000000041361501047226300200520ustar00rootroot00000000000000require_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-2.0.1/spec/commands_spec.rb000066400000000000000000000137231501047226300174300ustar00rootroot00000000000000require_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', # gh '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', # gh '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', # hub 'org/repo', ].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 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 # gh url = 'org/repo' it "canonicalizes short name #{url}" do expect(sj.send(:canonicalize_repo, url)). to eq('git@github.com:org/repo.git') end end end sugarjar-2.0.1/spec/repoconfig_spec.rb000066400000000000000000000123451501047226300177610ustar00rootroot00000000000000require_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', ], } allow(SugarJar::RepoConfig).to receive(:config_file?). and_return(true) allow(SugarJar::RepoConfig).to receive(:hash_from_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', }, } 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'], } %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', }, } %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', } %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-2.0.1/sugarjar.gemspec000066400000000000000000000022661501047226300165210ustar00rootroot00000000000000require_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