diff --git a/Makefile b/Makefile index 4d9bddb..2b93a00 100644 --- a/Makefile +++ b/Makefile @@ -1,43 +1,44 @@ -# exec-* targets execute the target targetly +# exec-* targets execute commands of the target directly # rest is executed in a container # -# stable container is meant to be used -# unstable is meant for development & testing +# stable container produces binaries which are meant to be used in production +# nightly container is meant for development & testing (b/c of clippy) # # TODO: cache build container: run it and exec statements inside # or figure out bind-mounted cargo cache -.PHONY=default compile build stable-environment unstable-environment stable-build unstable-build exec-stable-build exec-unstable-build test exec-test +.PHONY=default compile build stable-environment nightly-environment stable-build nightly-build exec-stable-build exec-nightly-build test exec-test RUST_STABLE_SPEC="1.15.1" -RUST_UNSTABLE_SPEC="nightly-2017-03-30" +# 05-21, last nightly used: -2017-03-30 +RUST_NIGHTLY_SPEC="nightly" DEPS=$(wildcard src/*.rs) CURRENT_USER="$(shell id -u)" STABLE_BUILD_IMAGE="${USER}/pretty-git-prompt" -UNSTABLE_BUILD_IMAGE="${USER}/pretty-git-prompt:dev" +NIGHTLY_BUILD_IMAGE="${USER}/pretty-git-prompt:dev" STABLE_CONTAINER_RUN=docker run --rm -v ${PWD}:/app:Z -ti $(STABLE_BUILD_IMAGE) -# breaks CI: -v ~/.cargo/registry/:/home/pretty/.cargo/registry/:Z -UNSTABLE_CONTAINER_RUN=docker run --rm -v ${PWD}:/app:Z -ti $(UNSTABLE_BUILD_IMAGE) +# breaks CI: -v ~/.cargo/registry/:/home/pretty/.cargo/registry/:Z +NIGHTLY_CONTAINER_RUN=docker run --rm -v ${PWD}:/app:Z -ti $(NIGHTLY_BUILD_IMAGE) default: build -compile: unstable-build +compile: nightly-build build: stable-build stable-environment: docker build --build-arg USER_ID=$(CURRENT_USER) --build-arg RUST_SPEC=$(RUST_STABLE_SPEC) --build-arg WITH_TEST=no --tag $(STABLE_BUILD_IMAGE) . -unstable-environment: - docker build --build-arg USER_ID=$(CURRENT_USER) --build-arg RUST_SPEC=$(RUST_UNSTABLE_SPEC) --build-arg WITH_TEST=yes --tag $(UNSTABLE_BUILD_IMAGE) . +nightly-environment: + docker build --build-arg USER_ID=$(CURRENT_USER) --build-arg RUST_SPEC=$(RUST_NIGHTLY_SPEC) --build-arg WITH_TEST=yes --tag $(NIGHTLY_BUILD_IMAGE) . stable-build: stable-environment $(STABLE_CONTAINER_RUN) make exec-stable-build -unstable-build: - $(UNSTABLE_CONTAINER_RUN) make exec-unstable-build +nightly-build: + $(NIGHTLY_CONTAINER_RUN) make exec-nightly-build exec-stable-build: target/release/pretty-git-prompt -exec-unstable-build: target/debug/pretty-git-prompt +exec-nightly-build: target/debug/pretty-git-prompt target/release/pretty-git-prompt: $(DEPS) LIBZ_SYS_STATIC=1 cargo build --release @@ -46,7 +47,7 @@ target/debug/pretty-git-prompt: $(DEPS) test: - $(UNSTABLE_CONTAINER_RUN) make exec-test + $(NIGHTLY_CONTAINER_RUN) make exec-test exec-test: target/debug/pretty-git-prompt py.test-3 -vv tests @@ -56,13 +57,13 @@ exec-test: target/debug/pretty-git-prompt # compile and inject into container # open prompt with prepared git repo zsh-demo: - $(UNSTABLE_CONTAINER_RUN) files/demo.py zsh + $(NIGHTLY_CONTAINER_RUN) files/demo.py zsh bash-demo: - $(UNSTABLE_CONTAINER_RUN) files/demo.py bash + $(NIGHTLY_CONTAINER_RUN) files/demo.py bash shell: - $(UNSTABLE_CONTAINER_RUN) zsh -l + $(NIGHTLY_CONTAINER_RUN) zsh -l show-work: egrep --color=yes -C 3 "(TODO|FIXME)" $(DEPS) Makefile Dockerfile diff --git a/README.md b/README.md index 7a655bd..e8b1863 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,32 @@ Your current git repository information inside a beautiful shell prompt. ![Preview of pretty-git-prompt](/data/example.png) +Features: + + * You are able to display values such as: + * git repository state (resolving `merge` conflict, interactive `rebase`, ...) + * Current branch name. + * Count of changed, newly-added, staged, conflicting files. + * You can track divergence against arbitrary branches. + * Every value in output can be fully configured via a config file. + * Sample configuration files feature colors. + * The tool supports `zsh` and `bash`. + * pretty-git-prompt is written in Rust programming language and is delivered as a single, statically-linked binary. + ## Development status -I would like to produce the first official 0.1.0 release, but there are still -some issues I need to clear out. So use at your own risk. +The tool is ready to use. + +## How can I try this out? -## How can I try this? +Very easily! You don't need to install pretty-git-prompt if you just want to +see it in action. There is a make target which launches docker container with +whole environment set up. -Very easily, actually! It just takes some time to prepare it (create build -environment, compile the tool, run the demo). +It just takes some time to prepare the environment (create build environment, +compile the tool, run the demo). Just clone this git repository @@ -35,25 +50,28 @@ $ make zsh-demo And this is what you should see: -![]() TBD +![Preview using zsh.](/data/zsh-screenshot.png) This is an interactive shell, so you can play with it. ### bash +In case you want to see the tool in bash shell: + ``` $ make bash-demo ``` -![]() TBD +![Preview using bash.](/data/bash-screenshot.png) -This demo is one of the ways I test the tool. +This demo is one of the ways I verify that the tool works correctly. -## Usage +## Installation -If you want to add it inside your shell, this section contains information how to do that. +If you want to add pretty-git-prompt inside your shell, this section contains +information how to do that. ### Obtaining `pretty-git-prompt` binary @@ -61,12 +79,21 @@ If you want to add it inside your shell, this section contains information how t #### GitHub release -Get latest binary via GitHub release: +Get the binary via [latest GitHub release](https://github.com/TomasTomecek/pretty-git-prompt/releases/latest). + +For a linux distrubution: ``` -$ curl -O TBD +$ curl -O https://github.com/TomasTomecek/pretty-git-prompt/releases/download/0.1.3/pretty-git-prompt-0.1.0-x86_64-unknown-linux-gnu ``` +Or for MacOS: + +``` +$ curl -O https://github.com/TomasTomecek/pretty-git-prompt/releases/download/0.1.0/pretty-git-prompt-0.1.0-x86_64-apple-darwin +``` + + #### Compile it yourself @@ -80,14 +107,14 @@ If you have rust compiler and cargo available on your system, you can compile the tool without using a container: ``` -$ cargo build --release +$ make exec-stable-build ``` The binary is then available on this path: ``` $ ls -lha target/release/pretty-git-prompt --rwxr-xr-x 2 user group 4.8M Apr 1 21:37 target/release/pretty-git-prompt +-rwxr-xr-x 2 user group 1.7M May 9 21:37 target/release/pretty-git-prompt ``` @@ -145,10 +172,11 @@ This is not a git repository: Error { code: -3, klass: 6, message: "could not fi ## Configuration The configuration is documented inside default config file. Therefore it's not -explicitely written down here. You can obtain it via: +explicitly written down here. You can obtain it via: ``` $ pretty-git-prompt create-default-config +Configuration file created at "/home/you/.config/pretty-git-prompt.yml" ``` This repository contains also configuration for bash and zsh with colors: @@ -174,7 +202,7 @@ This project builds upon several principles: 1. Configurable as much as possible. 2. Pretty and useful. 3. As few dependencies as possible. - 4. Easy to contibute to: + 4. Easy to contribute to: * Build with a single command. * Build inside predictive environment. * Test with a single command. @@ -189,10 +217,10 @@ All you need is [docker](https://github.com/docker/docker) engine running and `m First you need to build container image with rust and all dependencies inside: ``` -$ make unstable-environment +$ make nightly-environment ``` -This is using latest usable nightly rust. +This is using latest nightly rust. The nightly is used because of [clippy](https://github.com/Manishearth/rust-clippy). And then just make sure all tests are passing and you are not introducing any new warnings: @@ -207,4 +235,4 @@ If any of the two `make` invocations above doesn't work for you, please open an This tool is heavily inspired by [zsh-git-prompt](https://github.com/olivierverdier/zsh-git-prompt). At some -point I realized, I wanted a more powerful tool. +point I realized, I wanted a more powerful tool so I wrote pretty-git-prompt. diff --git a/data/bash-screenshot.png b/data/bash-screenshot.png new file mode 100644 index 0000000..bfece0d Binary files /dev/null and b/data/bash-screenshot.png differ diff --git a/data/example.png b/data/example.png index 13331db..59cb0cc 100644 Binary files a/data/example.png and b/data/example.png differ diff --git a/data/zsh-screenshot.png b/data/zsh-screenshot.png new file mode 100644 index 0000000..e2cc58c Binary files /dev/null and b/data/zsh-screenshot.png differ diff --git a/files/demo.py b/files/demo.py index db79567..114431a 100755 --- a/files/demo.py +++ b/files/demo.py @@ -55,13 +55,16 @@ def checkout_ref(z, ref): def checkout_b(z, branch_name): z.sendline("git checkout -b {}".format(branch_name)) -def create_file(filename, content): - with open(filename, "w") as fd: - fd.write(content + "\r\n") +def append_file(z, filename, content): + z.sendline("echo '{}' >> {}".format(content, filename)) + # with open(filename, "a") as fd: + # fd.write(content + "\r\n") -def append_file(filename, content): - with open(filename, "a") as fd: - fd.write(content + "\r\n") +def create_file(z, filename, content): + z.sendline("touch {}".format(filename)) + append_file(z, filename, content) + # with open(filename, "w") as fd: + # fd.write(content + "\r\n") class G(): @@ -111,7 +114,7 @@ def run(self): class SimpleUntrackedFilesRepo(BareRepo): def run(self): super().run() - create_file("file.txt", "text") + create_file(self.z, "file.txt", "text") class SimpleChangedFilesRepo(SimpleUntrackedFilesRepo): @@ -130,7 +133,7 @@ def run(self): class SimpleDirtyWithCommitRepo(SimpleRepo): def run(self): super().run() - create_file("file.txt", "text2") + create_file(self.z, "file.txt", "text2") class RepoWithOrigin(SimpleRepo): @@ -145,7 +148,7 @@ def run(self): super().run() with self.s: push(self.z, "origin", "master", with_tracking=False) - create_file("file.txt", "text3") + create_file(self.z, "file.txt", "text3") with self.s: add_file(self.z, "file.txt") with self.s: @@ -167,7 +170,7 @@ def run(self): class RWORemoteCommits(RepoWithOrigin): def run(self): super().run() - create_file("file.txt", "text4") + create_file(self.z, "file.txt", "text4") with self.s: add_file(self.z, "file.txt") with self.s: @@ -192,7 +195,7 @@ def run(self): super().run() checkout_b(self.z, "branch") reset_hard(self.z, "HEAD^") - create_file("file.txt", "text5") + create_file(self.z, "file.txt", "text5") add_file(self.z, "file.txt") commit(self.z) checkout_ref(self.z, "master") @@ -204,7 +207,7 @@ class Demo(RWORemoteCommits): def run(self): super().run() with self.s: - create_file("file.txt", "text5") + create_file(self.z, "file.txt", "text5") with self.s: add_file(self.z, "file.txt") with self.s: @@ -214,7 +217,7 @@ def run(self): with self.s: push(self.z, "upstream", "master", with_tracking=False) with self.s: - append_file("file.txt", "text6") + append_file(self.z, "file.txt", "text6") with open("file.txt", "a") as f: f.write("text6") with self.s: @@ -222,14 +225,14 @@ def run(self): with self.s: commit(self.z) with self.s: - append_file("file.txt", "text7") + append_file(self.z, "file.txt", "text7") with self.s: add_file(self.z, "file.txt") with self.s: - append_file("file.txt", "text8") + append_file(self.z, "file.txt", "text8") with self.s: - create_file("file2.txt", "text7") - self.z.sendline() + create_file(self.z, "file2.txt", "text7") + # self.z.sendline() self.z.interact() diff --git a/hack/ci.sh b/hack/ci.sh index ac6c879..a4bed0c 100755 --- a/hack/ci.sh +++ b/hack/ci.sh @@ -3,7 +3,7 @@ if [ "${TRAVIS_EVENT_TYPE}" = "cron" -a "${TARGET}" = "x86_64-unknown-linux-gnu" ] ; then - make unstable-environment && make test + make nightly-environment && make test else cargo test --verbose fi diff --git a/src/conf.rs b/src/conf.rs index 5196b3e..0107a52 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,3 +1,7 @@ +/* This module handles configuration. Ideally it should depend only on constants. + * + */ + use std::fs::{File,OpenOptions}; use std::io; use std::io::{Write,Read}; @@ -122,6 +126,7 @@ struct Separator { display: String, } +// FIXME: this should be defined in models impl Separator { fn new(value_yaml: &Yaml, simple_value: &SimpleValue) -> Separator { let separator_display_mode = match value_yaml["display"].as_str() { @@ -167,6 +172,7 @@ impl Conf { if version.is_badvalue() || version.is_null() { panic!("'version' is missing in config file."); } + // there could be a better place to validate this match version.as_str() { Some(s) => { if s != CURRENT_CONFIG_VERSION { @@ -179,9 +185,12 @@ impl Conf { Conf { c: yaml.clone(), display_master: display_master } } - // TODO: create a function to return list of structs and pass that to display master + // FIXME: this is super-hacky and because of separators, since they need to know + // if there is a value surrounding them; ideally this would return an array of + // struct, which would hold common attributes and a reference to yaml, each value + // would be then validated pub fn populate_values(&mut self) -> String { - let ref values_yaml = self.c["values"]; + let values_yaml = &self.c["values"]; if values_yaml.is_badvalue() || values_yaml.is_null() { panic!("No values to display."); } @@ -192,36 +201,29 @@ impl Conf { // are we suppose to display a separator? let mut separator_pending: Option = None; + // FIXME: all of this logic should live outside of this module for v in values { - let simple_value = SimpleValue::new(&v); + let simple_value = SimpleValue::new(v); let value_type = simple_value.value_type.as_str(); if value_type == "separator" { - let separator = Separator::new(&v, &simple_value); + let separator = Separator::new(v, &simple_value); let separator_display = separator.display(); if separator.is_display_always() { out += &separator_display.unwrap(); } else { separator_pending = separator_display; } - } else { - match self.display_master.display_value(&v, &simple_value) { - Some(s) => { - // add separator if it is needed - match separator_pending.clone() { - Some(separator) => { - if prev_was_set { - // println!("add separator {:?}", simple_value); - out += &separator; - separator_pending = None; - } - }, - None => (), - } - out += &s; - prev_was_set = true; - }, - None => () + } else if let Some(s) = self.display_master.display_value(v, &simple_value) { + // add separator if it is needed + if let Some(separator) = separator_pending.clone() { + if prev_was_set { + // println!("add separator {:?}", simple_value); + out += &separator; + separator_pending = None; + } } + out += &s; + prev_was_set = true; } } out.clone() @@ -240,18 +242,18 @@ pub fn load_configuration_from_file>(path: P) -> Result, display_master: DisplayMaster) -> Conf { - let content: String; - if supplied_conf_path.is_some() { - content = match load_configuration_from_file(supplied_conf_path.unwrap()) { + let content: String = if supplied_conf_path.is_some() { + match load_configuration_from_file(supplied_conf_path.unwrap()) { Ok(c) => c, Err(e) => { println!("ERROR"); panic!("Couldn't open configuration file: {:?}", e); } - }; + } } else { - content = match load_configuration_from_file(get_default_config_path()) { + match load_configuration_from_file(get_default_config_path()) { Ok(c) => c, Err(e) => { let kind = e.kind(); @@ -262,13 +264,15 @@ pub fn get_configuration(supplied_conf_path: Option, display_master: Dis panic!("Couldn't open configuration file: {:?}", kind); } } - }; - } + } + }; let docs = YamlLoader::load_from_str(&content).unwrap(); Conf::new(docs[0].clone(), display_master) } -pub fn create_default_config(path: PathBuf) -> Result { +// take default config and write it to path of default config location +// error out if the config already exists +pub fn create_default_config(path: &PathBuf) -> Result { match OpenOptions::new() .write(true) .create_new(true) @@ -330,7 +334,7 @@ values: []"; remove_file(p.clone()); } - let result = create_default_config(p.clone()); + let result = create_default_config(&p); assert!(result.is_ok()); let mut file = File::open(p.clone()).unwrap(); @@ -350,7 +354,7 @@ values: []"; .open(p.clone()); assert!(Path::new(&p).exists()); - let result = create_default_config(p.clone()); + let result = create_default_config(&p); assert!(result.is_err()); remove_file(p.clone()); @@ -362,7 +366,7 @@ values: []"; remove_file(p.clone()); } - let result = create_default_config(p.clone()); + let result = create_default_config(&p); assert!(result.is_ok()); let repo = Repository::discover(".").unwrap(); diff --git a/src/main.rs b/src/main.rs index 7375ad9..cd39f23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +/* This module is suppose to be a glue between all other modules. + * + */ + extern crate clap; extern crate git2; extern crate yaml_rust; @@ -44,7 +48,7 @@ fn main() { if matches.is_present(CLI_DEFAULT_CONFIG_SUBC_NAME) { let p = get_default_config_path(); - match create_default_config(p.clone()) { + match create_default_config(&p) { Ok(path) => { println!("Configuration file created at \"{}\"", path); return (); diff --git a/src/models.rs b/src/models.rs index 0008c1b..ef3baec 100644 --- a/src/models.rs +++ b/src/models.rs @@ -16,10 +16,11 @@ pub trait Display { } +// used to substitute values like or fn substiute_special_values(s: String, values: &HashMap) -> String { let mut r:String = s; for (k, v) in values { - r = r.replace(k, &v); + r = r.replace(k, v); } r } @@ -29,6 +30,7 @@ pub fn format_value(pre_format: &str, post_format: &str, data: &str) -> String { } +// this is the minimum amount of required attributes of a value #[derive(Debug, Clone)] pub struct SimpleValue { pub value_type: String, @@ -114,9 +116,13 @@ impl<'a> FileStatus<'a> { h.insert("staged".to_string(), STAGED_KEY); h.insert("conflicts".to_string(), CONFLICTS_KEY); if let Some(s) = self.backend.get_file_status() { - match h.get(file_type.clone()) { - Some(v) => return s.get(v.clone()).cloned(), - None => panic!("Invalid name for file status: {}", file_type) + let ft_string: String = file_type.to_string(); + match h.get(&ft_string) { + Some(v) => { + let v_string: String = v.to_string(); + return s.get(&v_string).cloned(); + }, + None => panic!("Invalid name for file status: {}", &ft_string) }; } None @@ -172,13 +178,11 @@ impl<'a> Display for RemoteTracking<'a> { let mut response: String = "".to_string(); for value in self.values.clone() { - match self.display_value(value.clone(), a_b.clone(), - special_values.clone()) { - Some(s) => response += &s, - None => (), + if let Some(s) = self.display_value(value.clone(), a_b.clone(), special_values.clone()) { + response += &s } } - if response.len() > 0 { + if !response.is_empty() { Some(response) } else { None @@ -193,7 +197,7 @@ impl<'a> RemoteTracking<'a> { let remote_branch: Option = match value_yaml["remote_branch"].as_str() { Some(s) => { let remote_branch_string = s.to_string(); - let v: Vec<&str> = remote_branch_string.splitn(2, "/").collect(); + let v: Vec<&str> = remote_branch_string.splitn(2, '/').collect(); if v.len() != 2 { panic!("`remote_branch` must be in form of `/`"); } @@ -279,14 +283,14 @@ impl DisplayMaster { pub fn display_value(&self, value_yaml: &Yaml, simple_value: &SimpleValue) -> Option { let o: Option = match simple_value.value_type.as_str() { - "repository_state" => RepoStatus::new(&simple_value, &self.backend, self.debug).display(), + "repository_state" => RepoStatus::new(simple_value, &self.backend, self.debug).display(), "new" | "changed" | "staged" | - "conflicts" => FileStatus::new(&simple_value, &self.backend, self.debug).display(), + "conflicts" => FileStatus::new(simple_value, &self.backend, self.debug).display(), // separator is displayed in conf, pretty hacky // "separator" => Separator::new(&simple_value, self.debug).display(), - "remote_difference" => RemoteTracking::new(value_yaml, &simple_value, &self.backend, self.debug).display(), + "remote_difference" => RemoteTracking::new(value_yaml, simple_value, &self.backend, self.debug).display(), _ => panic!("Unknown value type: {:?}", value_yaml) }; o