diff --git a/.gitignore b/.gitignore index 880ac59..f9d62d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ build/ .DS_Store +.projectile +gb +TODO \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 6ce766d..0000000 --- a/Makefile +++ /dev/null @@ -1,79 +0,0 @@ -UNAME := $(shell uname -s) - -CC=gcc - -CFLAGS := -std=c99 -Wall -CFLAGS += $(shell pkg-config --libs libgit2 jansson) -CFLAGS += $(shell pkg-config --cflags libgit2 jansson) - -# Add -rpath option so that the dynamic linker knows where to find shared -# libraries and avoid having to set LD_LIBRARY_PATH. -# -# See http://stackoverflow.com/a/695684 for background on this. -ifeq ($(UNAME), Linux) -CFLAGS += -Wl,-rpath /usr/local/lib -endif - -TARGET=build/gb -SRC=src/main.c - -INSTALL_DIR=/usr/local/bin/gb - -.PHONY: install build - -all: build - -build: src/main.c - mkdir -p build/ - $(CC) -o $(TARGET) $(SRC) $(CFLAGS) - -install: man - cp $(TARGET) $(INSTALL_DIR) - -force: - touch $(SRC) - -# Mainly for use when developing -run: clean force build install - gb - -clean: - @rm -rf build/ - -man: - install -g 0 -o 0 -m 0644 gb.1 /usr/share/man/man1/gb.1 - rm -f /usr/share/man/man1/gb.1.gz - gzip -f /usr/share/man/man1/gb.1 - -deps: clean - @if [ `pkg-config --modversion jansson` == "2.7" ]; then \ - echo "jansson was found - skipping installation"; \ - else \ - echo "installing jansson" && \ - mkdir -p build && \ - cd build && \ - wget http://www.digip.org/jansson/releases/jansson-2.7.tar.gz && \ - tar xzf jansson-2.7.tar.gz && \ - cd jansson-2.7 && \ - ./configure && \ - make && \ - make check && \ - echo "sudo password required for 'sudo make install' in jansson" && \ - sudo make install; \ - fi; \ - - - @if [ `pkg-config --modversion libgit2` == "0.22.1" ]; then \ - echo "libgit2 was found - skipping installation"; \ - else \ - echo "installing libgit2" && \ - mkdir -p build && \ - cd build && \ - wget https://github.com/libgit2/libgit2/archive/v0.22.1.tar.gz && \ - tar xzf v0.22.1.tar.gz && \ - cd libgit2-0.22.1 && \ - mkdir build && \ - cd build && \ - cmake .. && \ - cmake --build . --target install; \ - fi diff --git a/README.md b/README.md index 0ef839d..85583a7 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,6 @@ The output is sorted in chronological order - your last modified branches appear ## Installation -The install script will do its best to install dependencies before compiling. - - curl -sSL https://raw.githubusercontent.com/vroy/gb/master/install | bash - -Or alternatively, after making sure that [cmake](http://www.cmake.org/) is installed: - - git clone git@github.com:vroy/gb.git - cd gb - make deps - make - sudo make install + brew install go + brew install libgit2 + go get github.com/vroy/gb diff --git a/gb.1 b/gb.1 deleted file mode 100644 index da53f3b..0000000 --- a/gb.1 +++ /dev/null @@ -1,34 +0,0 @@ -.\" Manpage for gb. -.\" Contact vincentroy8@gmail.com to correct errors or typos. - -.TH gb 1 "14 March 2015" "0.0.2" "gb man page" - -.SH NAME -gb \- List git branches with additional information about them. - -.SH SYNOPSIS -gb [--merged] [--no-merged] [-a ] [-b ] - -.SH OPTIONS - -.IP "-a " -only show branches that are commits ahead." - -.IP "-b " -only show branches that are commits behind. - -.IP "--merged" -only show branches that are merged. - -.IP "--no-merged" -only show branches that are not merged. - -.SH BUGS -No known bugs. - -.SH SEE ALSO - -https://github.com/vroy/gb - -.SH AUTHOR -Vincent Roy (vincentroy8@gmail.com) diff --git a/gb.go b/gb.go new file mode 100644 index 0000000..28e6f7f --- /dev/null +++ b/gb.go @@ -0,0 +1,301 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "sort" + "strings" + "time" + + ioutil "io/ioutil" + + git "github.com/libgit2/git2go" + "github.com/mgutz/ansi" +) + +var ( + Red string = ansi.ColorCode("red") + Yellow = ansi.ColorCode("yellow") + Green = ansi.ColorCode("green") +) + +const ( + BaseBranch string = "master" + + CachePath = ".git/go_gb_cache.json" +) + +func exit(msg string, args ...interface{}) { + msg = fmt.Sprintf(msg, args...) + fmt.Println(msg) + os.Exit(1) +} + +func NewRepo() *git.Repository { + repo, err := git.OpenRepository(".") + if err != nil { + wd, _ := os.Getwd() + exit("Could not open repository at '%s'", wd) + } + return repo +} + +func NewBranchIterator(repo *git.Repository) *git.BranchIterator { + i, err := repo.NewBranchIterator(git.BranchLocal) + if err != nil { + wd, _ := os.Getwd() + exit("Failed to list branches for '%s'", wd) + } + return i +} + +func LookupBaseOid(repo *git.Repository) *git.Oid { + base_branch, err := repo.LookupBranch(BaseBranch, git.BranchLocal) + if err != nil { + exit("Error looking up '%s'", BaseBranch) + } + + return base_branch.Target() +} + +type Comparison struct { + Repo *git.Repository + BaseOid *git.Oid + Branch *git.Branch + Oid *git.Oid + + IsMerged bool + Ahead int + Behind int +} + +func NewComparison(repo *git.Repository, base_oid *git.Oid, branch *git.Branch, store CacheStore) *Comparison { + c := new(Comparison) + + c.Repo = repo + c.BaseOid = base_oid + + c.Branch = branch + c.Oid = branch.Target() + + cache := store[c.CacheKey()] + + if cache != nil { + c.Ahead = cache.Ahead + c.Behind = cache.Behind + c.IsMerged = cache.IsMerged + } else { + c.IsMerged = false + c.Ahead = -1 + c.Behind = -1 + } + + return c +} + +func (c *Comparison) Name() string { + name, err := c.Branch.Name() + if err != nil { + exit("Can't get branch name for '%s'", c.Oid) + } + return name +} + +func (c *Comparison) IsHead() bool { + head, err := c.Branch.IsHead() + if err != nil { + exit("Can't get IsHead for '%s'", c.Name()) + } + return head +} + +func (c *Comparison) Commit() *git.Commit { + commit, err := c.Repo.LookupCommit(c.Oid) + if err != nil { + exit("Could not lookup commit '%s'.", c.Oid.String()) + } + return commit +} + +func (c *Comparison) ColorCode() string { + hours, _ := time.ParseDuration("336h") // two weeks + two_weeks := time.Now().Add(-hours) + + if c.IsHead() { + return Green + } else if c.When().Before(two_weeks) { + return Red + } else { + return Yellow + } +} + +func (c *Comparison) When() time.Time { + sig := c.Commit().Committer() + return sig.When +} + +func (c *Comparison) FormattedWhen() string { + return c.When().Format("2006-01-02 15:04PM") +} + +func (c *Comparison) CacheKey() string { + strs := []string{c.BaseOid.String(), c.Oid.String()} + return strings.Join(strs, "..") +} + +func (c *Comparison) SetIsMerged() { + if c.Oid.String() == c.BaseOid.String() { + c.IsMerged = true + } else { + merged, err := c.Repo.DescendantOf(c.BaseOid, c.Oid) + if err != nil { + exit("Could not get descendant of '%s' and '%s'.", c.BaseOid.String(), c.Oid.String()) + } + c.IsMerged = merged + } +} + +func (c *Comparison) SetAheadBehind() { + var err error + c.Ahead, c.Behind, err = c.Repo.AheadBehind(c.Oid, c.BaseOid) + if err != nil { + exit("Error getting ahead/behind", c.BaseOid.String()) + } +} + +func (c *Comparison) Execute() { + if c.Ahead > -1 && c.Behind > -1 { + return + } + + c.SetIsMerged() + c.SetAheadBehind() +} + +type Comparisons []*Comparison + +type ComparisonsByWhen Comparisons + +func (a ComparisonsByWhen) Len() int { + return len(a) +} + +func (a ComparisonsByWhen) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ComparisonsByWhen) Less(i, j int) bool { + return a[i].When().Unix() < a[j].When().Unix() +} + +type Options struct { + Ahead int + Behind int + Merged bool + NoMerged bool + ClearCache bool +} + +func NewOptions() *Options { + o := new(Options) + + flag.IntVar(&o.Ahead, "ahead", -1, "only show branches that are commits ahead.") + flag.IntVar(&o.Behind, "behind", -1, "only show branches that are commits behind.") + flag.BoolVar(&o.Merged, "merged", false, "only show branches that are merged.") + flag.BoolVar(&o.NoMerged, "no-merged", false, "only show branches that are not merged.") + flag.BoolVar(&o.ClearCache, "clear-cache", false, "clear cache of comparisons.") + + flag.Parse() + + return o +} + +type CacheStore map[string]*Comparison + +func NewCacheStore() CacheStore { + bits, err := ioutil.ReadFile(CachePath) + if err != nil { + // no-op: `cache.json` will be written on exit. + } + + y := make(CacheStore) + _ = json.Unmarshal(bits, &y) + + return y +} + +func (store *CacheStore) WriteToFile() error { + b, err := json.Marshal(store) + if err != nil { + fmt.Printf("Could not save cache to file.") + } + ioutil.WriteFile(CachePath, b, 0644) + return nil +} + +func main() { + opts := NewOptions() + + if opts.ClearCache { + os.Remove(CachePath) + } + + store := NewCacheStore() + + repo := NewRepo() + branch_iterator := NewBranchIterator(repo) + base_oid := LookupBaseOid(repo) + + comparisons := make(Comparisons, 0) + + // type BranchIteratorFunc func(*Branch, BranchType) error + branch_iterator.ForEach(func(branch *git.Branch, btype git.BranchType) error { + comp := NewComparison(repo, base_oid, branch, store) + comparisons = append(comparisons, comp) + return nil + }) + + sort.Sort(ComparisonsByWhen(comparisons)) + + for _, comp := range comparisons { + comp.Execute() + + merged_string := "" + if comp.IsMerged { + merged_string = "(merged)" + } + + if opts.Ahead != -1 && opts.Ahead != comp.Ahead { + continue + } + + if opts.Behind != -1 && opts.Behind != comp.Behind { + continue + } + + if opts.Merged && !comp.IsMerged { + continue + } + + if opts.NoMerged && comp.IsMerged { + continue + } + + fmt.Printf( + "%s%s | %-30s | behind: %4d | ahead: %4d %s\n", + comp.ColorCode(), + comp.FormattedWhen(), + comp.Name(), + comp.Behind, + comp.Ahead, + merged_string) + + store[comp.CacheKey()] = comp + } + + store.WriteToFile() + +} diff --git a/install b/install deleted file mode 100755 index dbde1a7..0000000 --- a/install +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -if which cmake >/dev/null -then - echo 'cmake is installed.' - -elif which apt-get >/dev/null -then - echo 'installing cmake with apt-get' - sudo apt-get install cmake - -elif which brew >/dev/null -then - echo 'installing cmake with brew' - brew install cmake - -else - echo 'Could not install cmake. Make sure that cmake is installed and in your $PATH.' - exit 1 - -fi - -wget https://github.com/vroy/gb/archive/v0.0.4.tar.gz -tar xzf v0.0.4.tar.gz -cd gb-0.0.4 - -sudo make deps -make -sudo make install diff --git a/src/main.c b/src/main.c deleted file mode 100644 index a4daa7d..0000000 --- a/src/main.c +++ /dev/null @@ -1,384 +0,0 @@ -// IO stuff, output, reading files, etc. -#include - -// atoi for arguments parsing, qsort, malloc, and more? -#include - -// String utilities like strcmp, strcpy, etc -#include - -// git bindings -#include - -// JSON library -#include - -// POSIX API. For gwtcwd. -#include - -// To have a reference to MAXPATHLEN -#include - -// Parse them options -#include - - -// Uncomment to compile with debugging output turned on. -// #define DEBUG 1 - -//Define bool type -typedef int bool; -enum { false, true }; - - -typedef struct gb_options { - int ahead_filter; - int behind_filter; - int help_flag; - int merged_flag; - int no_merged_flag; - int clear_cache_flag; -} gb_options; - -typedef struct gb_comparison { - char tip[41]; - char master_tip[41]; - git_oid tip_oid; - git_oid master_oid; - char name[200]; - char reference_name[200]; - long timestamp; - size_t ahead; - size_t behind; - int is_head; - size_t is_merged; -} gb_comparison; - - -// Globals are evil, but this is a short-live program that will never change -// context while it's running. -git_repository *gb_repo; -json_t *gb_json; -char *gb_cache_path; -gb_options *options; - - -void gb_options_init(int argc, char **argv) { - options = malloc(sizeof(gb_options)); - - // Set defaults - options->ahead_filter = -1; - options->behind_filter = -1; - options->help_flag = 0; - options->merged_flag = 0; - options->no_merged_flag = 0; - options->clear_cache_flag = 0; - - - int opt; - - while (1) { - struct option long_options[] = { - { "merged", no_argument, &options->merged_flag, 1 }, - { "no-merged", no_argument, &options->no_merged_flag, 1 }, - { "clear-cache", no_argument, &options->clear_cache_flag, 1 }, - { "help", no_argument, &options->help_flag, 1 } - }; - - int option_index = 0; - opt = getopt_long(argc, argv, "a:b:h", long_options, &option_index); - - switch(opt) { - case 'a': - options->ahead_filter = atoi(optarg); - break; - case 'b': - options->behind_filter = atoi(optarg); - break; - case 'h': - case '?': - options->help_flag = 1; - break; - } - - if (opt == -1) break; - } -} - - - -char *RED = "\e[0;31m"; -char *YELLOW = "\e[0;33m"; -char *GREEN = "\e[0;32m"; - -void gb_git_check_return(int rc, char *msg) { - if (rc != 0) { - fprintf(stderr, "%s. Code: %d\n", msg, rc); - exit(1); - } -} - - - - - -void gb_comparison_new(git_reference *ref, gb_comparison *comp) { - int rc; - - memset(comp->tip, '\0', 41); - memset(comp->master_tip, '\0', 41); - - // Find branch name. - const char *name; - git_branch_name(&name, ref); - memset(comp->name, '\0', 200); - strcpy(comp->name, name); - - // Assign full reference name. - memset(comp->reference_name, '\0', 200); - strcat(comp->reference_name, "refs/heads/"); - strcat(comp->reference_name, comp->name); - - // Choose color of output. - comp->is_head = git_branch_is_head(ref); - - // Find tip oid. - rc = git_reference_name_to_id(&comp->tip_oid, gb_repo, comp->reference_name); - gb_git_check_return(rc, "Can't find branch tip id."); - git_oid_tostr(comp->tip, 41, &comp->tip_oid); - - // Keep reference to master_tip that we're comparing to. - rc = git_reference_name_to_id(&comp->master_oid, gb_repo, "refs/heads/master"); - gb_git_check_return(rc, "Can't find branch tip id."); - git_oid_tostr(comp->master_tip, 41, &comp->master_oid); - - // Find commit based on tip oid. - git_commit *commit; - git_commit_lookup(&commit, gb_repo, &(comp->tip_oid)); - - // Assign timestamp. - comp->timestamp = git_commit_time(commit); - - comp->ahead = 0; - comp->behind = 0; - comp->is_merged = 0; -} - - -int gb_comparison_asc_timestamp_sort(const void *a, const void *b) { - gb_comparison *x = *(gb_comparison **) a; - gb_comparison *y = *(gb_comparison **) b; - - if (x->timestamp < y->timestamp) return 1; - if (x->timestamp > y->timestamp) return -1; - - return 0; -} - -int gb_comparison_desc_timestamp_sort(const void *a, const void *b) { - gb_comparison *x = *(gb_comparison **) a; - gb_comparison *y = *(gb_comparison **) b; - - if (x->timestamp > y->timestamp) return 1; - if (x->timestamp < y->timestamp) return -1; - - return 0; -} - - -void gb_comparison_execute(gb_comparison *comp) { - char *range = malloc( (strlen(comp->tip) + strlen(comp->master_tip) + 3) * sizeof(char)); - sprintf(range, "%s..%s", comp->tip, comp->master_tip); - - json_t *object = json_object_get(gb_json, range); - if (json_is_object(object)) { - json_unpack(object, "{sIsIsI}", - "ahead", &comp->ahead, - "behind", &comp->behind, - "is_merged", &comp->is_merged); - - } else { - // Same behaviour for is_merged as `git branch --merged` - comp->is_merged = git_graph_descendant_of(gb_repo, &comp->master_oid, &comp->tip_oid) || git_oid_equal(&comp->master_oid, &comp->tip_oid); - - git_graph_ahead_behind(&comp->ahead, &comp->behind, gb_repo, &comp->tip_oid, &comp->master_oid); - - json_object_set(gb_json, range, json_pack("{sIsIsI}", "ahead", comp->ahead, "behind", comp->behind, "is_merged", comp->is_merged)); - } -} - - -// Returns a pointer to the color constant based on some very basic rules. -char* gb_output_color(gb_comparison *comp) { - if ( !isatty(STDOUT_FILENO) ) return ""; - - time_t rawtime = comp->timestamp; - time_t now = time(0); - int one_week = (14 * 24 * 60 * 60); - - if (comp->is_head) { - return GREEN; - } else if ( rawtime > (now - one_week) ) { - return YELLOW; - } else { - return RED; - } -} - -void gb_comparison_print(gb_comparison *comp) { - char formatted_time[80]; - time_t rawtime = comp->timestamp; - struct tm * timeinfo = localtime(&rawtime); - strftime(formatted_time, 80, "%F %H:%M%p", timeinfo); - - char merge_status[10]; - memset(merge_status, '\0', 9); - - if (comp->is_merged) { - strcpy(merge_status, " (merged)"); - } - - printf("%s%s | %-40.40s | behind: %4lu | ahead: %4lu %s\n", - gb_output_color(comp), - formatted_time, - comp->name, - comp->behind, - comp->ahead, - merge_status); -} - -bool gb_branch_is_filtered(gb_comparison *comp) { - if (options->ahead_filter > -1 && comp->ahead != options->ahead_filter) { - return true; - } - - if (options->behind_filter > -1 && comp->behind != options->behind_filter) { - return true; - } - - if (options->merged_flag && !comp->is_merged) { - return true; - } - - if (options->no_merged_flag && comp->is_merged) { - return true; - } - - return false; -} - - - -void print_last_branches() { - gb_comparison **comps = malloc( sizeof(gb_comparison*) ); - - int branch_count = 0; - - git_branch_iterator *iter; - int rc; - - rc = git_branch_iterator_new(&iter, gb_repo, GIT_BRANCH_LOCAL); - gb_git_check_return(rc, "Can't iterate over branches."); - - git_reference *ref = NULL; - git_branch_t type; - - while (!(rc = git_branch_next(&ref, &type, iter))) { - comps = (gb_comparison**) realloc(comps, (branch_count+1) * sizeof(gb_comparison*)); - gb_comparison *comp = malloc(sizeof(gb_comparison)); - gb_comparison_new(ref, comp); - comps[branch_count] = comp; - branch_count++; - } - - qsort(comps, branch_count, sizeof(*comps), gb_comparison_desc_timestamp_sort); - - for (int i = 0; i < branch_count; i++) { - gb_comparison_execute(comps[i]); - if (!gb_branch_is_filtered(comps[i])) { - gb_comparison_print(comps[i]); - } - } - - git_branch_iterator_free(iter); - -} - -void gb_cache_load() { - json_error_t error; - - if (options->clear_cache_flag) { - gb_json = json_object(); - return; - } - - gb_json = json_load_file(gb_cache_path, 0, &error); - - if (!gb_json) { - #ifdef DEBUG - if (error.line > 0) { - fprintf(stderr, "error: on line %d: %s\n", error.line, error.text); - } else { - fprintf(stderr, "error: %s\n", error.text); - } - #endif - - // If JSON load failed (file does not exist, syntax error, etc), simply - // proceed forward with an empty json_object. - gb_json = json_object(); - } -} - -void gb_cache_dump() { - json_dump_file(gb_json, gb_cache_path, 0); -} - - -git_repository* gb_git_repo_new() { - git_repository *repo; - char cwd[MAXPATHLEN]; - - if (getcwd(cwd, sizeof(cwd)) == NULL) { - fprintf(stderr, "fatal: Could not get current working directory.\n"); - exit(1); - } - - int rc = git_repository_open_ext(&repo, cwd, 0, NULL); - if (rc == GIT_ENOTFOUND) { - fprintf(stderr, "fatal: Not a git repository (or any of the parent directories): .git\n"); - exit(1); - } - - gb_git_check_return(rc, "opening repository"); - - return repo; -} - - - - -int main(int argc, char **argv) { - git_libgit2_init(); - - gb_options_init(argc, argv); - - if (options->help_flag) { - system("man gb"); - return 0; - } - - // First thing we do is init/load the globals. - gb_repo = gb_git_repo_new(); - - gb_cache_path = malloc(MAXPATHLEN * sizeof(char)); - sprintf(gb_cache_path, "%sgb_cache.json", git_repository_path(gb_repo)); - - gb_cache_load(); - - // Program run. - print_last_branches(); - - gb_cache_dump(); - - return 0; -}