├── .Rbuildignore ├── .gitignore ├── .travis.yml ├── .workbch ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── R ├── job_create.R ├── job_current.R ├── job_gitreport.R ├── job_glimpse.R ├── job_home.R ├── job_list.R ├── job_modify.R ├── job_open.R ├── job_openurl.R ├── job_seek.R ├── miscellaneous.R ├── new.R ├── pull.R ├── readwrite.R ├── sentinel.R ├── update.R ├── verify.R └── zzz.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── docs ├── 404.html ├── CNAME ├── LICENSE-text.html ├── LICENSE.html ├── articles │ ├── gitrep.gif │ ├── gitrep.mp4 │ ├── index.html │ ├── modify.gif │ ├── modify.mp4 │ ├── moving.gif │ ├── moving.mp4 │ ├── navigation.gif │ ├── navigation.mp4 │ ├── setup.gif │ ├── setup.html │ ├── setup.mp4 │ └── usage.html ├── authors.html ├── docsearch.css ├── docsearch.js ├── index.html ├── link.svg ├── pkgdown.css ├── pkgdown.js ├── pkgdown.yml └── reference │ ├── index.html │ ├── job_create.html │ ├── job_gitreport.html │ ├── job_glimpse.html │ ├── job_home.html │ ├── job_list.html │ ├── job_modify.html │ ├── job_open.html │ ├── job_openurl.html │ └── job_seek.html ├── man ├── job_create.Rd ├── job_gitreport.Rd ├── job_glimpse.Rd ├── job_home.Rd ├── job_list.Rd ├── job_modify.Rd ├── job_open.Rd ├── job_openurl.Rd └── job_seek.Rd ├── tests ├── testthat.R └── testthat │ ├── test-job_create.R │ ├── test-job_glimpse.R │ ├── test-job_home.R │ ├── test-job_list.R │ ├── test-job_modify.R │ ├── test-miscellaneous.R │ ├── test-pull.R │ ├── test-readwrite.R │ ├── test-sentinel.R │ └── test-verify.R ├── vignettes ├── .gitignore ├── setup.Rmd ├── setup.mp4 └── usage.Rmd └── workbch.Rproj /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^workbch\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^LICENSE\.md$ 5 | ^_pkgdown\.yml$ 6 | ^docs$ 7 | ^pkgdown$ 8 | ^\.travis\.yml$ 9 | ^codecov\.yml$ 10 | ^misc$ 11 | ^\.workbch$ 12 | ^vignettes/setup\.Rmd$ 13 | ^vignettes/usage\.Rmd$ 14 | ^vignettes/.*\.mp4$ 15 | ^vignettes/.*\.gif$ 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | inst/doc 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | cache: packages 5 | after_success: 6 | - Rscript -e 'covr::codecov()' 7 | -------------------------------------------------------------------------------- /.workbch: -------------------------------------------------------------------------------- 1 | workbch 2 | JqUHsERxGy 3 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: workbch 2 | Title: Lightweight Project Management 3 | Version: 0.1.0 4 | Authors@R: 5 | person(given = "Danielle", 6 | family = "Navarro", 7 | role = c("aut", "cre"), 8 | email = "d.navarro@unsw.edu.au", 9 | comment = c(ORCID = "0000-0001-7648-6578")) 10 | Description: A "work bench" to manage projects from within R. 11 | License: MIT + file LICENSE 12 | Encoding: UTF-8 13 | LazyData: true 14 | URL: https://github.com/djnavarro/workbch 15 | BugReports: https://github.com/djnavarro/workbch/issues 16 | Imports: 17 | jsonlite, 18 | dplyr, 19 | tibble, 20 | readr, 21 | purrr, 22 | rstudioapi, 23 | git2r, 24 | janitor, 25 | rmarkdown, 26 | knitr, 27 | kableExtra, 28 | lubridate, 29 | rlang 30 | RoxygenNote: 6.1.1 31 | Suggests: 32 | testthat (>= 2.1.0), 33 | covr 34 | VignetteBuilder: knitr 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019 2 | COPYRIGHT HOLDER: Danielle Navarro 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Danielle Navarro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,wkbch_tbl) 4 | export(job_create) 5 | export(job_gitreport) 6 | export(job_glimpse) 7 | export(job_home) 8 | export(job_list) 9 | export(job_modify) 10 | export(job_open) 11 | export(job_openurl) 12 | export(job_seek) 13 | importFrom(rlang,"%||%") 14 | -------------------------------------------------------------------------------- /R/job_create.R: -------------------------------------------------------------------------------- 1 | #' Create a new job 2 | #' 3 | #' @param jobname name of the job to create 4 | #' @param description brief description of the job 5 | #' @param status should be "active", "inactive", "complete", "abandoned", "masked" 6 | #' @param owner should be a name or a nickname 7 | #' @param priority numeric 8 | #' @param tags a string containing comma separated list of tags 9 | #' @param path path to the job home directory 10 | #' 11 | #' @details The role of the \code{job_create()} function is to create new workbch job. 12 | #' It can be called in two ways, interactively or programmatically. To call the 13 | #' function interactively, R must be in interactive mode and the function should 14 | #' be called with no arguments specified. When called in this fashion the user 15 | #' will be presented with a sequence of prompts, asking them to specify each 16 | #' of the parameters that define a job (e.g., a character string for \code{jobname}, 17 | #' a number for \code{priority}). When used interactively, you do not need to include 18 | #' quote marks when entering a string: \code{job_create()} will coerce the input to 19 | #' the appropriate format, and then append the created job to the job file. 20 | #' 21 | #' When called programmatically, the user must specify the arguments in the 22 | #' call to \code{job_create()}. The \code{jobname}, \code{description} and 23 | #' \code{owner} arguments should be character strings of length 1, and all three 24 | #' are mandatory. The \code{status} for a job should be one of the following 25 | #' values: \code{"active"}, \code{"inactive"}, \code{"complete"}, \code{"abandoned"} 26 | #' or \code{"masked"}. The \code{priority} for a job should be a positive integer: 27 | #' the intent is that priority 1 is the highest priority, followed by priority 2, 28 | #' and so one. The \code{tags} for a job can be specified as a single string, using 29 | #' \code{|} as a separator character (e.g., \code{tags = "research | statistics"} 30 | #' would create two tags for the job). Finally, the \code{path} should specify the 31 | #' location of a folder containing the project files. 32 | #' 33 | #' For non-mandatory arguments, if the user does not specify a value, the 34 | #' \code{job_create()} function applies the following defaults: \code{priority = 1}, 35 | #' \code{status = "active"}, \code{tags = character(0)} and \code{path = NA}`. 36 | #' 37 | #' Note that, although jobs can also be associated with URLs (e.g., link to a 38 | #' GitHub repository or a document on Overleaf), the \code{job_create()} function 39 | #' does not (at this time) allow you to specify URLs. These can be added using 40 | #' \code{job_modify()}. 41 | #' 42 | #' @return Invisibly returns a list containing the parameters for the job 43 | #' @export 44 | job_create <- function(jobname = NULL, description = NULL, owner = NULL, 45 | status = NULL, priority = NULL, tags = NULL, 46 | path = NULL) { 47 | 48 | use_prompt <- is.null(jobname) & is.null(description) & is.null(owner) & 49 | is.null(status) & is.null(priority) & is.null(tags) & is.null(path) & 50 | interactive() 51 | 52 | if(use_prompt) { 53 | 54 | cat("\nDetails of the new job:\n") 55 | cat("(Press enter to skip or use default values)\n\n") 56 | 57 | # elicit responses from user 58 | jobname <- readline( " Job name............ ") 59 | description <- readline( " Description......... ") 60 | owner <- readline( " Owner............... ") 61 | status <- readline( " Status.............. ") 62 | priority <- as.numeric(readline( " Priority............ ")) 63 | tags <- readline( " Tags (comma separated) ........... ") 64 | path <- readline( " Path................ ") 65 | 66 | # treat no response as default value 67 | if(jobname == "") jobname <- NULL 68 | if(description == "") description <- NULL 69 | if(owner == "") owner <- NULL 70 | if(status == "") status <- NULL 71 | if(is.na(priority)) priority <- NULL 72 | if(length(tags) == 0) tags <- NULL 73 | if(path == "") path <- NULL 74 | } 75 | 76 | if(is.null(jobname)) stop("'jobname' cannot be empty", call. = FALSE) 77 | if(is.null(description)) stop("'description' cannot be empty", call. = FALSE) 78 | if(is.null(owner)) stop("'owner' cannot be empty", call. = FALSE) 79 | 80 | # read jobs file and check the names of the jobs 81 | jobs <- job_read() 82 | job_names <- pull_jobnames(jobs) 83 | 84 | # make sure no job exists with this name 85 | verify_jobname(jobname) 86 | verify_jobmissing(jobname, jobs) 87 | 88 | # split the tags if necessary 89 | if(!is.null(tags)) {tags <- split_tags(tags)} 90 | 91 | # append the new job 92 | jb <- new_job( 93 | jobname = jobname, 94 | description = description, 95 | owner = owner, 96 | status = status, 97 | priority = priority, 98 | tags = tags, 99 | path = path, 100 | urls = empty_url() 101 | ) 102 | jobs[[jobname]] <- jb 103 | 104 | # write the file 105 | job_write(jobs) 106 | 107 | # invisibly returns the created job 108 | return(invisible(jb)) 109 | } 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /R/job_current.R: -------------------------------------------------------------------------------- 1 | 2 | job_getcurrent <- function(jobs) { 3 | 4 | # search heuristic: 5 | # - look first the active RStudio project 6 | # - if that doesn't do it, look at working directory 7 | # - if that doesn't do it, throw error 8 | 9 | # get all job names and paths 10 | job_names <- pull_jobnames(jobs) 11 | job_paths <- pull_jobpaths(jobs) 12 | 13 | # restrict to jobs with non-NA paths & normalise 14 | known <- !is.na(job_paths) 15 | job_paths <- normalizePath(job_paths[known]) 16 | job_names <- job_names[known] 17 | 18 | # preferentially use the RStudio project path 19 | project_path <- NULL 20 | if(rstudioapi::isAvailable()) { 21 | project_path <- rstudioapi::getActiveProject() # NULL if no project 22 | } 23 | 24 | # attempt to match against project path & return if successful 25 | if(!is.null(project_path)) { 26 | match_ind <- which(job_paths == project_path) 27 | if(length(match_ind) > 0) { 28 | matched_job <- job_names[match_ind] 29 | message("using job '", matched_job, "'") 30 | return(matched_job) 31 | } 32 | } 33 | 34 | # attempt to match against the working directory & return if successful 35 | working_dir <- getwd() 36 | split_job_paths <- strsplit(job_paths, .Platform$file.sep) 37 | split_wd <- strsplit(working_dir, .Platform$file.sep)[[1]] 38 | 39 | # find match 40 | match_lgl <- purrr::map_lgl(split_job_paths, function(x) { 41 | len <- length(x); 42 | if(length(split_wd) < len) { 43 | return(FALSE) # if wd is shorter than job path, it doesn't match 44 | } 45 | return(identical(x, split_wd[1:len])) # if wd is a sub dir it matches 46 | }) 47 | match_ind <- which(match_lgl) 48 | 49 | if(length(match_ind) == 1) { 50 | matched_job <- job_names[match_ind] 51 | message("using job '", matched_job, "'") 52 | return(matched_job) 53 | } 54 | 55 | stop("Could not detect current job", call. = FALSE) 56 | } 57 | -------------------------------------------------------------------------------- /R/job_gitreport.R: -------------------------------------------------------------------------------- 1 | 2 | #' Report the git status of all jobs 3 | #' 4 | #' @param show_clean should clean repositories be included in the output? 5 | #' 6 | #' @details The role of the \code{git_report()} function is to provide an overview 7 | #' of the status of all workbch jobs that are associated with a git repository. 8 | #' For every job, it uses \code{git2r::in_repository} to determine if the job 9 | #' folder (i.e., the \code{path} for that job) is in a git repository. Jobs that are 10 | #' not in git repositories are ignored. 11 | #' 12 | #' For jobs that are associated with git repositories, the \code{git_report()} 13 | #' function calls \code{git2r::status()} to determine the git status. If there is an 14 | #' upstream set (i.e., \code{git2r::branch_get_upstream()} detects an upstream 15 | #' repository), it will also call \code{git2r::ahead_behind()} to determine how many 16 | #' commits the local repository is ahead and/or behind of the upstream. 17 | #' 18 | #' By default, no output is shown for clean repositories (\code{show_clean = FALSE}). 19 | #' A repository is deemed to be clean if there are no staged, unstaged or 20 | #' untracked files and it is neither ahead nor behind the upstream repository. 21 | #' If the user specifies \code{show_clean = TRUE}, then results are reported for every 22 | #' job that is linked to a git repository. 23 | #' 24 | #' @return A tibble with columns \code{jobname}, \code{staged}, \code{unstaged}, 25 | #' \code{untracked}, \code{ahead} and \code{behind}. The \code{jobname} column is 26 | #' a character vector, all others are integer valued. 27 | #' 28 | #' @export 29 | job_gitreport <- function(show_clean = FALSE) { 30 | 31 | # get the job locations 32 | jobs <- job_read() 33 | proj <- pull_jobinfo(jobs) 34 | x <- list() 35 | 36 | for(i in 1:nrow(proj)) { 37 | 38 | # for the sake of my sanity 39 | pp <- proj$path[i] 40 | pn <- proj$jobname[i] 41 | 42 | if(git2r::in_repository(pp)) { 43 | 44 | # get the branch 45 | repo_head <- git2r::repository_head(pp) 46 | upstream_head <- git2r::branch_get_upstream(repo_head) 47 | 48 | # number of commits ahead and behind 49 | if(is.null(upstream_head)) { 50 | repo_ab <- c(NA, NA) 51 | } else { 52 | repo_ab <- git2r::ahead_behind( 53 | local = repo_head, 54 | upstream = upstream_head 55 | ) 56 | } 57 | 58 | # get the repo git status 59 | repo_status <- purrr::map_dfr(unclass(git2r::status(pp)), length) 60 | 61 | # put it all together in a tibble 62 | x[[i]] <- dplyr::bind_cols( 63 | tibble::tibble(jobname = pn), 64 | repo_status, 65 | tibble::tibble(ahead = repo_ab[1], behind = repo_ab[2]) 66 | ) 67 | } 68 | } 69 | 70 | # collapse to a single tibble 71 | gitst <- dplyr::bind_rows(x) 72 | 73 | if(!show_clean) { 74 | gitst <- dplyr::filter(gitst, 75 | !(staged == 0 & unstaged == 0 & untracked == 0 & 76 | (is.na(ahead) | ahead == 0) & (is.na(behind) | behind == 0)) 77 | ) 78 | } 79 | 80 | return(as_wkbch_tbl(gitst)) 81 | 82 | } 83 | -------------------------------------------------------------------------------- /R/job_glimpse.R: -------------------------------------------------------------------------------- 1 | 2 | #' View the information stored about a job 3 | #' 4 | #' @param jobname Name of the job to show (defaults to the current job) 5 | #' 6 | #' @details The \code{job_glimpse()} function displays parameter values for the 7 | #' job specified in the \code{jobname} argument. If no argument is specified 8 | #' \code{job_glimpse()} will attempt to guess the "current" job by looking at 9 | #' any open RStudio projects. If no project is open (or the RStudio API is not 10 | #' available) it attempts to guess by looking at the working directory. The 11 | #' output is presented to the user as a message. 12 | #' 13 | #' @return Invisibly returns the job as a list 14 | #' @export 15 | job_glimpse <- function(jobname = NULL) { 16 | 17 | # read jobs and use the current job if the user does 18 | # not specify an argument 19 | jobs <- job_read() 20 | if(is.null(jobname)) { 21 | jobname <- suppressMessages(job_getcurrent(jobs)) 22 | } else { 23 | verify_jobname(jobname) 24 | verify_jobexists(jobname, jobs) 25 | } 26 | 27 | # obtain the job 28 | jb <- jobs[[jobname]] 29 | 30 | msg_str <- "" 31 | msg_str <- paste0(msg_str, "\n", collapse = "") 32 | msg_str <- paste0(msg_str, " jobname: ", jb$jobname, "\n", collapse = "") 33 | msg_str <- paste0(msg_str, " description: ", jb$jobname, "\n", collapse = "") 34 | msg_str <- paste0(msg_str, " owner: ", jb$owner, "\n", collapse = "") 35 | msg_str <- paste0(msg_str, " priority: ", jb$priority, "\n", collapse = "") 36 | msg_str <- paste0(msg_str, " status: ", jb$status, "\n", collapse = "") 37 | msg_str <- paste0(msg_str, " path: ", jb$path, "\n", collapse = "") 38 | msg_str <- paste0(msg_str, " tags: ", paste0(jb$tags, collapse = " | "), "\n", collapse = "") 39 | if(nrow(jb$urls) > 0) { 40 | for(i in 1:nrow(jb$urls)) { 41 | site <- jb$urls$site[i] 42 | npad <- max(13-nchar(site), 1) 43 | pad <- paste(rep(" ", npad), collapse = "") 44 | msg_str <- paste0(msg_str, " ", site, ":", pad, jb$urls$link[i], "\n", collapse = "") 45 | } 46 | } 47 | 48 | message(msg_str) 49 | return(invisible(jb)) 50 | } 51 | -------------------------------------------------------------------------------- /R/job_home.R: -------------------------------------------------------------------------------- 1 | 2 | #' Path to the job home 3 | #' 4 | #' @param jobname Name of job (defaults to current job) 5 | #' 6 | #' @details The \code{job_home()} function returns the file path for `jobname`. 7 | #' This can be useful if you need to specify the location of files relative to 8 | #' a project. 9 | #' 10 | #' If no argument is specified \code{job_home()} will attempt to guess the 11 | #' current job by looking at any open RStudio projects. If no project is open 12 | #' or the RStudio API is not available it attempts to guess by looking at the 13 | #' working directory. 14 | #' 15 | #' @return Path to the job folder as a character string 16 | #' 17 | #' @export 18 | job_home <- function(jobname = NULL) { 19 | 20 | # read jobs and use the current job if the user does 21 | # not specify an argument 22 | jobs <- job_read() 23 | if(is.null(jobname)) { 24 | jobname <- suppressMessages(job_getcurrent(jobs)) 25 | } else { 26 | verify_jobname(jobname) 27 | verify_jobexists(jobname, jobs) 28 | } 29 | 30 | path <- jobs[[jobname]]$path 31 | if(length(path) == 0 || is.na(path)) { 32 | stop("No path known for job '", jobname, "'", call. = FALSE) 33 | } 34 | return(normalizePath(path)) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /R/job_list.R: -------------------------------------------------------------------------------- 1 | #' View a list of all jobs 2 | #' 3 | #' @param search input query used to extract a subset of jobs 4 | #' @param select character vector of columns to return 5 | #' 6 | #' @details The \code{job_list()} function is used to display a summary of the 7 | #' jobs known to workbch, shown as a tibble with one row per job. By default 8 | #' only those jobs with a \code{priority} value of 1 or 2 will be shown, and 9 | #' only if their \code{status} is "active" or "inactive". Jobs that do not have 10 | #' sufficient priority or whose status is "complete", "abandoned" or "masked" 11 | #' are not shown. 12 | #' 13 | #' The default behaviour can be overridden by specifying a \code{search} string (or 14 | #' object that can be coerced to a string). If the user specifies a value to 15 | #' \code{search} then \code{job_list()} attempts to match the search string with a parameter 16 | #' value. For instance \code{job_list("active")} will return all jobs that have status 17 | #' \code{active} and \code{job_list(1)} will return all jobs that have priority 1. At 18 | #' present this functionality requires that the \code{search} string be an exact value 19 | #' (e.g., \code{search = "Dani"} would not match a job owned by \code{"Danielle"}), 20 | #' though this may be extended in the future. 21 | #' 22 | #' The output is returned to the user as a tibble, and by default the columns 23 | #' displayed are \code{jobname}, \code{owner}, \code{priority}, \code{status}, 24 | #' and \code{description}. The user can directly specify which columns to be 25 | #' included using the \code{select} argument, which should be a character vector 26 | #' specifying the names of the columns to include. In addition to the five 27 | #' columns made available by default, the output can include the \code{path}, 28 | #' \code{tags} and \code{urls} associated with the job. For convenience, if you 29 | #' set \code{select = NULL} then all columns will be returned. 30 | #' 31 | #' @return An object of class worbch_tbl. It is regular tibble with a slightly 32 | #' modified print method that ensures all rows are printed in the output 33 | #' @export 34 | job_list <- function( 35 | search = NULL, 36 | select = c("jobname", "owner", "priority", "status", "description") 37 | ){ 38 | 39 | # read jobs 40 | jobs <- job_read() 41 | 42 | # if there are no jobs, return null invisibly 43 | if(is.null(jobs)) { 44 | message("No known jobs") 45 | return(invisible(NULL)) 46 | } 47 | 48 | # construct tibble with the simple fields 49 | job_tbl <- purrr::map_df(jobs, function(x){ 50 | tibble::as_tibble(x[c("jobname", "owner", "priority", "status", 51 | "description", "path")])}) 52 | 53 | # add tags 54 | job_tbl$tags <- purrr::map_chr(jobs, function(x) { 55 | paste0(x$tags, collapse = " | ") 56 | }) 57 | 58 | # add urls 59 | job_tbl$urls <- purrr::map(jobs, function(x) { 60 | x$urls 61 | }) 62 | 63 | # drop rows as needed 64 | if(is.null(search)) { 65 | job_tbl <- job_tbl[job_tbl$status %in% c("active", "inactive"), ] 66 | job_tbl <- job_tbl[job_tbl$priority %in% 1:2, ] 67 | } else { 68 | target <- as.character(search) 69 | keep <- purrr::map_lgl(jobs, function(jb) { 70 | job_flat <- unname(unlist(unclass(jb))) 71 | if(any(job_flat == target, na.rm = TRUE)) {return(TRUE)} 72 | return(FALSE) 73 | }) 74 | job_tbl <- job_tbl[keep, ] 75 | } 76 | 77 | #if(!is.null(status)) job_tbl <- job_tbl[job_tbl$status %in% status, ] 78 | #if(!is.null(priority)) job_tbl <- job_tbl[job_tbl$priority %in% priority, ] 79 | #if(!is.null(owner)) job_tbl <- job_tbl[job_tbl$owner %in% owner, ] 80 | 81 | if(nrow(job_tbl) == 0) return(NULL) 82 | 83 | # arrange 84 | job_tbl <- dplyr::arrange(job_tbl, priority, status, owner, jobname) 85 | 86 | # drop cols as needed 87 | if(!is.null(select)) job_tbl <- job_tbl[, select] 88 | 89 | # look for missing sentinels 90 | missing <- sentinel_missing() 91 | 92 | # throw warning if there are missing sentinel files 93 | if(length(missing) > 0) { 94 | warning( 95 | "Some job folders have moved or been deleted. Use job_seek() to fix", 96 | call. = FALSE 97 | ) 98 | } 99 | return(as_wkbch_tbl(job_tbl)) 100 | } 101 | 102 | -------------------------------------------------------------------------------- /R/job_modify.R: -------------------------------------------------------------------------------- 1 | 2 | #' Modify the properties of a job 3 | #' 4 | #' @param jobname the current name for the job 5 | #' @param newname the new name 6 | #' @param description the new description 7 | #' @param owner nick name for the new owner 8 | #' @param status the new status 9 | #' @param priority the new priority 10 | #' @param path the new path to the job folder 11 | #' @param tags string with tags (uses | as separator) 12 | #' @param url string specifying url 13 | #' @param delete should this job be deleted (default = FALSE) 14 | #' 15 | #' @details The role of the \code{job_modify()} function is to change one or more 16 | #' parameters of an existing workbch job. It can be called in two ways, interactively 17 | #' or programmatically. To call the function interactively, R must be in interactive 18 | #' mode and the function should be called specifying either the \code{jobname} argument 19 | #' only, or no arguments specified. If no \code{jobname} is specified the 20 | #' \code{job_modify()} function will attempt to guess the 21 | #' current job by looking at any open RStudio projects. If no project is open 22 | #' or the RStudio API is not available it attempts to guess by looking at the 23 | #' working directory 24 | #' 25 | #' When called interactiely, the user 26 | #' will be presented with a sequence of prompts, asking them to specify each 27 | #' of the parameters that define a job (e.g., a character string for \code{descripton}, 28 | #' a number for \code{priority}). When used interactively, you do not need to include 29 | #' quote marks when entering a string: \code{job_modify()} will coerce the input to 30 | #' the appropriate format, and then append the created job to the job file. 31 | #' 32 | #' When called programmatically, the user must specify the arguments in the 33 | #' call to \code{job_modify()}. Onlt the \code{jobname} argument is mandatory. 34 | #' To rename an existing job the \code{newname} argument should be specified. 35 | #' To modify \code{description}, \code{owner}, \code{status}, \code{priority}, 36 | #' \code{path}, \code{tags} or \code{urls} the relevant argument shoul be 37 | #' specified. 38 | #' 39 | #' The \code{status} of a job should be \code{"active"}, \code{"inactive"}, 40 | #' \code{"complete"}, \code{"abandoned"} or \code{"masked"}. The \code{priority} for 41 | #' a job should be a positive integer: the intent is that priority 1 is the highest 42 | #' priority, followed by priority 2, and so one. The \code{tags} for a job can be 43 | #' specified as a single string, using \code{|} as a separator character (e.g., 44 | #' \code{tags = "research | statistics"} would create two tags for the job). 45 | #' The \code{path} should specify the location of a folder containing the project 46 | #' files. The format for \code{urls} is the same as for tags. For example to 47 | #' specify a GitHub link one might write \code{urls = "github \ 48 | #' https://github.com/djnavarro/workbch"}. Multiple URLs can be specified by 49 | #' extending this syntax using \code{|} as the separator. For example: 50 | #' \code{urls = "github \ https://github.com/djnavarro/workbch | docs | 51 | #' https://djnavarro.github.io/workbch"}. 52 | #' 53 | #' Setting \code{delete = TRUE} will delete the job. 54 | #' 55 | #' @export 56 | job_modify <- function( 57 | jobname = NULL, newname = NULL, description = NULL, owner = NULL, 58 | status = NULL, priority = NULL, path = NULL, tags = NULL, url = NULL, 59 | delete = FALSE 60 | ){ 61 | 62 | # use prompt method if we are interactive and the user has 63 | # not specified any arguments (except possibly jobname) 64 | use_prompt <- interactive() & is.null(newname) & 65 | is.null(description) & is.null(owner) & is.null(status) & 66 | is.null(priority) & is.null(path) & is.null(tags) & is.null(url) & 67 | delete == FALSE 68 | 69 | # interactive version 70 | if(use_prompt) { 71 | 72 | # read jobs and use the current job if the user does 73 | # not specify an argument 74 | jobs <- job_read() 75 | if(is.null(jobname)) { 76 | jobname <- suppressMessages(job_getcurrent(jobs)) 77 | } 78 | 79 | cat("Current details for job:", jobname, "\n") 80 | job_glimpse(jobname) 81 | cat("\n") 82 | 83 | cat("What do you want to do?\n\n") 84 | cat(" [1] change the job name\n") 85 | cat(" [2] change the description\n") 86 | cat(" [3] change the job owner\n") 87 | cat(" [4] change the job status\n") 88 | cat(" [5] change the job priority\n") 89 | cat(" [6] change the job location\n") 90 | cat(" [7] add or remove a url\n") 91 | cat(" [8] change the tags\n") 92 | cat(" [9] delete this job\n") 93 | cat("\n") 94 | ans <- readline("Selection: ") 95 | ans <- suppressWarnings(as.numeric(ans)) 96 | cat("\n") 97 | if(ans == 1) return(prompt_rename(jobname)) 98 | if(ans == 2) return(prompt_description(jobname)) 99 | if(ans == 3) return(prompt_owner(jobname)) 100 | if(ans == 4) return(prompt_status(jobname)) 101 | if(ans == 5) return(prompt_priority(jobname)) 102 | if(ans == 6) return(prompt_path(jobname)) 103 | if(ans == 7) return(prompt_url(jobname)) 104 | if(ans == 8) return(prompt_tag(jobname)) 105 | if(ans == 9) return(prompt_delete(jobname)) 106 | return(invisible(NULL)) 107 | 108 | # programmatic version 109 | } else { 110 | 111 | if(is.null(jobname)) { 112 | stop("'jobname' is required", call. = FALSE) 113 | } 114 | 115 | jobs <- update_job( 116 | jobname = jobname, 117 | newname = newname, 118 | description = description, 119 | owner = owner, 120 | status = status, 121 | priority = priority, 122 | path = path, 123 | tags = tags, 124 | url = url, 125 | delete = delete 126 | ) 127 | job_write(jobs) 128 | return(invisible(NULL)) 129 | 130 | } 131 | } 132 | 133 | prompt_rename <- function(jobname) { 134 | newname <- readline("Enter new job name: ") 135 | job_write(update_job(jobname = jobname, newname = newname)) 136 | } 137 | 138 | prompt_description <- function(jobname) { 139 | description <- readline("Enter new job description: ") 140 | job_write(update_job(jobname = jobname, description = description)) 141 | } 142 | 143 | prompt_owner <- function(jobname) { 144 | description <- readline("Enter new job owner: ") 145 | job_write(update_job(jobname = jobname, owner = owner)) 146 | } 147 | 148 | prompt_delete <- function(jobname) { 149 | jobname2 <- readline("To confirm deletion, type the job name: ") 150 | if(jobname2 == jobname) { 151 | job_write(update_job(jobname = jobname, delete = TRUE)) 152 | } else { 153 | message("Deletion aborted") 154 | } 155 | } 156 | 157 | prompt_priority <- function(jobname) { 158 | priority <- readline("Enter new job priority: ") 159 | job_write(update_job(jobname = jobname, priority = priority)) 160 | } 161 | 162 | prompt_path <- function(jobname) { 163 | priority <- readline("Enter new job path: ") 164 | job_write(update_job(jobname = jobname, path = path)) 165 | } 166 | 167 | prompt_url <- function(jobname) { 168 | url <- readline("Enter new url, in 'site | url' format: ") 169 | job_write(update_job(jobname = jobname, url = url)) 170 | } 171 | 172 | prompt_tag <- function(jobname) { 173 | tags <- readline("Enter new tags, in 'tag | tag' format: ") 174 | job_write(update_job(jobname = jobname, tags = tags)) 175 | } 176 | 177 | prompt_status <- function(jobname) { 178 | status <- readline("Enter new job status: ") 179 | job_write(update_job(jobname = jobname, status = status)) 180 | } 181 | 182 | -------------------------------------------------------------------------------- /R/job_open.R: -------------------------------------------------------------------------------- 1 | #' Navigate to a job 2 | #' 3 | #' @param jobname name of job to open 4 | #' @details The \code{job_open()} function opens a workbch job. The meaning of 5 | #' "open" here depends on context. If there is an RStudio project linked to the 6 | #' job and the RStudio API is available, then \code{job_open()} will switch the 7 | #' to the RStudio project that \code{jobname} is linked to. If this is not 8 | #' possible an there is a path associated with the job, then \code{job_open()} 9 | #' will change the working directory to that path. If neither option is possible 10 | #' then this function does nothing. 11 | #' 12 | #' Note that when working interactively, \code{job_open()} can be called without 13 | #' specfying the \code{jobname}. In this case the user will be presented with 14 | #' prompts to select the desired job. 15 | #' @export 16 | job_open <- function(jobname = NULL) { 17 | 18 | # read the jobs & verify the name 19 | jobs <- job_read() 20 | 21 | # if the user doesn't specify a job, prompt them 22 | jobname <- jobname %||% prompt_jobname() 23 | 24 | # check jobname 25 | verify_jobname(jobname) 26 | verify_jobexists(jobname, jobs) 27 | 28 | # if we're not in RStudio, just change working directory 29 | if(!rstudioapi::isAvailable()) { 30 | message("setting working directory to ", jobs[[jobname]]$path) 31 | setwd(jobs[[jobname]]$path) 32 | return(invisible(NULL)) 33 | } 34 | 35 | # if we're in RStudio... read files in job and look for Rproj file 36 | files <- list.files(jobs[[jobname]]$path) 37 | rproj <- grep(".*\\.Rproj$", files) 38 | 39 | # if there's a project we can switch to, do so... 40 | if(length(rproj) > 0) { 41 | rstudioapi::openProject(jobs[[jobname]]$path) 42 | return(invisible(NULL)) 43 | } 44 | 45 | # if not, just change working directory... 46 | #rstudioapi::executeCommand("closeProject") 47 | message("setting working directory to ", jobs[[jobname]]$path) 48 | setwd(jobs[[jobname]]$path) 49 | return(invisible(NULL)) 50 | 51 | } 52 | 53 | prompt_jobname <- function() { 54 | opts <- job_list()$jobname # todo... don't call user facing function!!!! 55 | if(length(opts)>9) opts <- opts[1:9] 56 | for(i in 1:length(opts)) { 57 | cat(i, ":", opts[i], "\n") 58 | } 59 | cat("other: please type jobname\n") 60 | jnum <- readline("which job? ") 61 | if(jnum %in% as.character(1:length(opts))) { 62 | jobname <- opts[as.numeric(jnum)] 63 | } else { 64 | jobname <- jnum 65 | } 66 | return(jobname) 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /R/job_openurl.R: -------------------------------------------------------------------------------- 1 | 2 | #' Navigate to a URL associated with a job 3 | #' 4 | #' @param site label denoting the site (e.g., "github") 5 | #' @param jobname name of the job 6 | #' 7 | #' @details The \code{job_openurl()} function opens a URL associated with a 8 | #' job in a browser window. The \code{site} argument is the site nickname 9 | #' (e.g. "github", "homepage", etc) and the \code{jobname} argument specifies 10 | #' the job to use. 11 | #' 12 | #' If no argument is specified \code{job_openurl()} will attempt to guess the 13 | #' current job by looking at any open RStudio projects. If no project is open 14 | #' or the RStudio API is not available it attempts to guess by looking at the 15 | #' working directory. 16 | #' 17 | #' @export 18 | job_openurl <- function(site, jobname = NULL) { 19 | 20 | # read the jobs & verify the name 21 | jobs <- job_read() 22 | jobname <- jobname %||% job_getcurrent(jobs) 23 | 24 | # check jobname 25 | verify_jobname(jobname) 26 | verify_jobexists(jobname, jobs) 27 | 28 | urls <- jobs[[jobname]]$urls 29 | if(!(site %in% urls$site)) { 30 | stop("'", jobname, "' does not have a link for site '", site, "'", call. = FALSE) 31 | } 32 | link <- urls$link[urls$site == site] 33 | utils::browseURL(link) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /R/miscellaneous.R: -------------------------------------------------------------------------------- 1 | 2 | split_tags <- function(tags) { 3 | verify_onestring(tags) 4 | tags <- strsplit(tags, "|", fixed = TRUE)[[1]] 5 | tags <- trimws(tags, which = "both") 6 | tags <- tags[tags != ""] 7 | return(tags) 8 | } 9 | 10 | split_url <- function(url) { 11 | verify_onestring(url) 12 | url <- strsplit(url, "|", fixed = TRUE)[[1]] 13 | url <- trimws(url, which = "both") 14 | url <- url[url != ""] 15 | return(url) 16 | } 17 | 18 | empty_url <- function() { 19 | new_url(site = character(0), link = character(0), verify = FALSE) 20 | } 21 | 22 | idstring <- function() { 23 | paste0(sample(c(letters, LETTERS), 10, TRUE), collapse="") 24 | } 25 | 26 | # Specify a print method for a workbench tibble 27 | #' @export 28 | print.wkbch_tbl <- function(x, n = 100, ...) { 29 | class(x) <- setdiff(class(x), "wkbch_tbl") 30 | print(x, n = n, ...) 31 | } 32 | 33 | # A very bad way to coerce tibble to workbench tibble 34 | as_wkbch_tbl <- function(x) { 35 | class(x) <- c("wkbch_tbl", class(x)) 36 | return(x) 37 | } 38 | 39 | #' @importFrom rlang %||% 40 | NULL 41 | 42 | # # returns a list of expressions 43 | # capture_dots <- function(...) { 44 | # as.list(substitute(list(...)))[-1L] 45 | # } 46 | 47 | -------------------------------------------------------------------------------- /R/new.R: -------------------------------------------------------------------------------- 1 | 2 | new_job <- function(jobname, description, owner, status = NULL, 3 | priority = NULL, tags = NULL, path = NULL, urls = NULL, 4 | idstring = NULL, verify = TRUE, sentinel = TRUE) { 5 | 6 | # replace nulls with default values 7 | status <- status %||% "active" 8 | priority <- priority %||% 1 9 | tags <- tags %||% character(0) 10 | path <- path %||% NA_character_ 11 | urls <- urls %||% empty_url() 12 | idstring <- idstring %||% idstring() 13 | 14 | # verify unless explicitly told not to 15 | if(verify) { 16 | verify_jobname(jobname) 17 | verify_description(description) 18 | verify_owner(owner) 19 | verify_status(status) 20 | verify_character(tags) 21 | verify_path(path) 22 | verify_priority(priority) 23 | ## missing: verify_urls(urls) 24 | } 25 | 26 | # construct object 27 | obj <- list( 28 | jobname = jobname, 29 | description = description, 30 | owner = owner, 31 | status = status, 32 | priority = priority, 33 | tags = tags, 34 | path = path, 35 | urls = urls, 36 | idstring = idstring 37 | ) 38 | 39 | # add sentinel file 40 | if(dir.exists(obj$path) & sentinel == TRUE) { 41 | sentinel_write(obj$path, obj$jobname, obj$idstring) 42 | } 43 | 44 | return(obj) 45 | } 46 | 47 | 48 | new_url <- function(site, link, verify = TRUE) { 49 | 50 | # verify unless explicitly told not to 51 | if(verify) { 52 | verify_site(site) 53 | verify_link(link) 54 | } 55 | 56 | # construct object 57 | tibble::tibble( 58 | site = site, 59 | link = link 60 | ) 61 | } 62 | 63 | -------------------------------------------------------------------------------- /R/pull.R: -------------------------------------------------------------------------------- 1 | 2 | # helper functions that take a list of jobs as input 3 | # and "pull" handy information as the output 4 | 5 | # returns a character vector of all job names 6 | pull_jobnames <- function(jobs) { 7 | if(is.null(jobs)) { return(character(0)) } 8 | return(purrr::map_chr(jobs, function(j) {j$jobname})) 9 | } 10 | 11 | # returns a character vector of all job paths 12 | pull_jobpaths <- function(jobs) { 13 | if(is.null(jobs)) { return(character(0)) } 14 | return(purrr::map_chr(jobs, function(j) { 15 | normalizePath(j$path, mustWork = FALSE) 16 | })) 17 | } 18 | 19 | # returns a tibble with jobname, path and idstring, only 20 | # for those jobs that contain a path. technically this 21 | # isn't analogous to dplyr::pull but whatever 22 | pull_jobinfo <- function(jobs) { 23 | job_tbl <- purrr::map_df(jobs, function(x){ 24 | if(!is.null(x$path)) { 25 | return(tibble::as_tibble( 26 | x[c("jobname", "path", "idstring")] 27 | )) 28 | } else { 29 | return(tibble::tibble( 30 | jobname = character(0), 31 | path = character(0), 32 | idstring = character(0) 33 | )) 34 | } 35 | }) 36 | job_tbl <- dplyr::arrange(job_tbl, jobname) 37 | job_tbl <- dplyr::filter(job_tbl, !is.na(path)) 38 | 39 | return(as_wkbch_tbl(job_tbl)) 40 | } 41 | -------------------------------------------------------------------------------- /R/readwrite.R: -------------------------------------------------------------------------------- 1 | 2 | # returns the path to the workbch jobs file 3 | job_file <- function() { 4 | file.path(getOption("workbch.home"), "workbch_jobs.json") 5 | } 6 | 7 | # write project data to JSON file (TODO: this can be condensed to 8 | # jsonlite::writeJSON presumably) 9 | job_write <- function(jobs) { 10 | job_str <- jsonlite::toJSON(jobs, pretty = TRUE) 11 | writeLines(job_str, job_file()) 12 | } 13 | 14 | # read project data from JSON file, and preprocess to deal with 15 | # limitations to JSON storage 16 | job_read <- function() { 17 | if(file.exists(job_file())) { 18 | 19 | jobs <- jsonlite::fromJSON(job_file()) 20 | jobs <- purrr::map(jobs, function(j) { 21 | 22 | # empty lists become data frames in some cases 23 | if(class(j$urls) == "list") {j$urls <- empty_url()} 24 | 25 | # empty lists become character vectors in others 26 | if(class(j$team) == "list") {j$team <- character(0)} 27 | if(class(j$tags) == "list") {j$tags <- character(0)} 28 | 29 | if(class(j$path) == "logical") { 30 | j$path <- as.character(j$path) 31 | } 32 | 33 | # don't let tags become matrices 34 | if(class(j$tags) == "matrix") {j$tags <- as.vector(j$tags)} 35 | 36 | # coerce to tibbles 37 | j$urls <- tibble::as_tibble(j$urls) 38 | 39 | return(j) 40 | }) 41 | return(jobs) 42 | } 43 | return(NULL) 44 | } 45 | -------------------------------------------------------------------------------- /R/sentinel.R: -------------------------------------------------------------------------------- 1 | 2 | # write one sentinel file 3 | sentinel_write <- function(dir, jobname, idstring) { 4 | 5 | verify_onestring(dir) 6 | verify_onestring(jobname) 7 | verify_onestring(idstring) 8 | 9 | dir <- normalizePath(dir) 10 | file <- normalizePath(file.path(dir, ".workbch"), mustWork = FALSE) 11 | writeLines(text = c(jobname, idstring), con = file) 12 | } 13 | 14 | 15 | # jobnames for which sentinels have been lost 16 | sentinel_missing <- function() { 17 | jobs <- job_read() 18 | dat <- pull_jobinfo(jobs) 19 | info <- purrr::transpose(dat) 20 | missing <- purrr::map_chr(info, function(job) { 21 | f <- file.path(normalizePath(job$path, mustWork = FALSE), ".workbch") 22 | if(file.exists(f)) { 23 | s <- readLines(f) 24 | if(s[1] == job$jobname & s[2] == job$idstring) { 25 | return("") 26 | } 27 | } 28 | return(job$jobname) 29 | }) 30 | missing <- missing[missing != ""] 31 | return(missing) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /R/update.R: -------------------------------------------------------------------------------- 1 | 2 | # anything that updates a job passes through here ---------------------------- 3 | 4 | update_job <- function( 5 | jobname, newname = NULL, description = NULL, owner = NULL, 6 | status = NULL, priority = NULL, path = NULL, tags = NULL, 7 | url = NULL, delete = FALSE 8 | ){ 9 | 10 | # note: update_job does not call a constructor function, it modifies an 11 | # existing job in place, so it has to call verify_ functions for anything 12 | # it modifies. it doesn't do the modification itself, merely verfies the 13 | # inputs where appropriate and passes it off to the relevant updater function 14 | 15 | jobs <- job_read() 16 | 17 | # verify job exists 18 | verify_jobname(jobname) 19 | verify_jobexists(jobname, jobs) 20 | 21 | # ------- delete -------- 22 | if(delete == TRUE) { 23 | jobs <- delete_job(jobs, jobname) 24 | return(jobs) 25 | } 26 | 27 | # ------- job name ------- 28 | if(!is.null(newname)) { 29 | verify_jobname(newname) 30 | verify_jobmissing(newname, jobs) 31 | jobs <- update_jobname(jobs, jobname, newname) 32 | jobname <- newname # in case of multiple changes 33 | } 34 | 35 | # ------- job description ------- 36 | if(!is.null(description)) { 37 | verify_description(description) 38 | jobs <- update_jobother(jobs, jobname, description) 39 | } 40 | 41 | # ------- job status ------- 42 | if(!is.null(status)) { 43 | verify_status(status) 44 | jobs <- update_jobother(jobs, jobname, status) 45 | } 46 | 47 | # ------- job owner ------- 48 | if(!is.null(owner)) { 49 | verify_character(owner) 50 | jobs <- update_jobother(jobs, jobname, owner) 51 | } 52 | 53 | # ------- job priority ------- 54 | if(!is.null(priority)) { 55 | verify_priority(priority) 56 | jobs <- update_jobother(jobs, jobname, priority) 57 | } 58 | 59 | # ------- job path ------- 60 | if(!is.null(path)) { 61 | verify_path(path) 62 | jobs <- update_jobpath(jobs, jobname, path) 63 | } 64 | 65 | # ------- job url ------- 66 | if(!is.null(url)) { 67 | url <- split_url(url) 68 | site <- url[1] 69 | link <- url[2] 70 | verify_site(site) 71 | if(is.na(link) | link == "") { 72 | jobs <- delete_joburl(jobs, jobname, site) 73 | } else { 74 | verify_link(link) 75 | jobs <- update_joburl(jobs, jobname, site, link) 76 | } 77 | } 78 | 79 | # ------- job tag -------- 80 | if(!is.null(tags)) { 81 | verify_character(tags) 82 | jobs <- update_jobtags(jobs, jobname, tags) 83 | } 84 | 85 | return(jobs) 86 | } 87 | 88 | # updater functions ----------------------------------------------------- 89 | 90 | # simple cases can be handled this way 91 | update_jobother <- function(jobs, jobname, value) { 92 | field <- deparse(substitute(value)) 93 | jobs[[jobname]][field] <- value 94 | return(jobs) 95 | } 96 | 97 | 98 | # path 99 | update_jobpath <- function(jobs, jobname, path) { 100 | jobs[[jobname]]$path <- path 101 | if(dir.exists(path)) { 102 | sentinel_write(path, jobname, jobs[[jobname]]$idstring) 103 | } 104 | return(jobs) 105 | } 106 | 107 | # jobname 108 | update_jobname <- function(jobs, jobname, newname) { 109 | job_names <- names(jobs) 110 | ind <- which(job_names == jobname) 111 | jb <- jobs[[jobname]] 112 | jb$jobname <- newname 113 | jobs <- jobs[-ind] 114 | jobs[[newname]] <- jb 115 | return(jobs) 116 | } 117 | 118 | # path 119 | update_jobtags <- function(jobs, jobname, tags) { 120 | jobs[[jobname]]$tags <- split_tags(tags) 121 | return(jobs) 122 | } 123 | 124 | # url 125 | update_joburl <- function(jobs, jobname, site, link) { 126 | 127 | urls <- jobs[[jobname]]$urls 128 | ind <- which(urls$site == site) 129 | if(length(ind) == 0) { 130 | urls <- dplyr::bind_rows(urls, new_url(site, link)) 131 | } else if(length(ind) == 1) { 132 | urls$link[ind] <- link 133 | } else stop("inconceivable!") # unreachable? 134 | 135 | urls <- dplyr::arrange(urls, site) 136 | jobs[[jobname]]$urls <- urls 137 | return(jobs) 138 | } 139 | 140 | 141 | # deletion functions ----------------------------------------------------- 142 | 143 | 144 | delete_job <- function(jobs, jobname) { 145 | jobs[[jobname]] <- NULL 146 | return(jobs) 147 | } 148 | 149 | delete_joburl <- function(jobs, jobname, site) { 150 | jb <- jobs[[jobname]] 151 | if(site %in% jb$urls$site) { 152 | jb$urls <- dplyr::filter(jb$urls, site != {{site}}) 153 | jobs[[jobname]] <- jb 154 | } else { 155 | warning("There is '", site, "' link in '", jobname, "'", call. = FALSE) 156 | } 157 | return(jobs) 158 | } 159 | 160 | -------------------------------------------------------------------------------- /R/verify.R: -------------------------------------------------------------------------------- 1 | # validator functions 2 | 3 | 4 | # generic checks ---------------------------------------------------------- 5 | 6 | # a lot of the validation checks involve verifying an an input argument 7 | # is length one character so let's not duplicate code my dear... 8 | verify_onestring <- function(object) { 9 | # tests for length, character, and lack-of-dimension 10 | if(length(object) != 1 | !is.character(object) | !is.null(dim(object))) { 11 | stop(deparse(substitute(object)), " must be character and length 1", 12 | call. = FALSE) 13 | } 14 | return(invisible(TRUE)) 15 | } 16 | 17 | # similar deal as above really 18 | verify_character <- function(object) { 19 | if(!is.character(object) | !is.null(dim(object))) { 20 | stop(deparse(substitute(object)), " must be character", 21 | call. = FALSE) 22 | } 23 | return(invisible(TRUE)) 24 | } 25 | 26 | 27 | # argument checks --------------------------------------------------------- 28 | 29 | 30 | # throw error if the jobname is invalid 31 | verify_jobname <- function(jobname) { 32 | verify_onestring(jobname) 33 | } 34 | 35 | # throw error if the path is invalid 36 | verify_path <- function(path) { 37 | verify_onestring(path) 38 | } 39 | 40 | # throw error if an unknown status is used 41 | verify_status <- function(status) { 42 | verify_onestring(status) 43 | 44 | if(!(status %in% c("active", "inactive", "complete", "abandoned", "masked"))) { 45 | stop("job status must be 'active', 'inactive', 'complete', 'abandoned' or 'masked'", call. = FALSE) 46 | } 47 | return(invisible(TRUE)) 48 | } 49 | 50 | # throw error if a priority is not a positive integer 51 | verify_priority <- function(priority) { 52 | if(length(priority) != 1) { 53 | stop("job priority must be a positive integer", call. = FALSE) 54 | } 55 | if(is.na(priority) | is.nan(priority) | is.infinite(priority)) { 56 | stop("job priority must be a positive integer", call. = FALSE) 57 | } 58 | if(!is.numeric(priority)) { 59 | stop("job priority must be a positive integer", call. = FALSE) 60 | } 61 | if(priority != as.integer(priority)) { 62 | stop("job priority must be a positive integer", call. = FALSE) 63 | } 64 | if(priority < 1) { 65 | stop("job priority must be a positive integer", call. = FALSE) 66 | } 67 | return(invisible(TRUE)) 68 | } 69 | 70 | # throw error if description is not a length 1 character 71 | verify_description <- function(description) { 72 | verify_onestring(description) 73 | } 74 | 75 | verify_site <- function(site) { 76 | verify_onestring(site) 77 | } 78 | 79 | verify_link <- function(link) { 80 | verify_onestring(link) 81 | } 82 | 83 | verify_owner <- function(owner) { 84 | verify_onestring(owner) 85 | } 86 | 87 | 88 | # status checks ----------------------------------------------------------- 89 | 90 | # ensure there is no job called jobname 91 | verify_jobmissing <- function(jobname, jobs, strict = TRUE) { 92 | missing <- !(jobname %in% pull_jobnames(jobs)) 93 | if(strict & missing == FALSE) { 94 | stop("a job already exists with name '", jobname, "'", call. = FALSE) 95 | } 96 | return(invisible(TRUE)) 97 | } 98 | 99 | # ensure there is no job called jobname 100 | verify_jobexists <- function(jobname, jobs, strict = TRUE) { 101 | present <- jobname %in% pull_jobnames(jobs) 102 | if(strict & present == FALSE) { 103 | stop("No job exists with name '", jobname, "'", call. = FALSE) 104 | } 105 | return(invisible(TRUE)) 106 | } 107 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | # to avoid the 'no visible binding' note during check: 2 | utils::globalVariables( 3 | c("priority", "status", "owner", "jobname", "staged", "unstaged", "untracked", 4 | "ahead", "behind", "path", "hidden", "jobs", "tag") 5 | ) 6 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | # workbch 16 | 17 | 18 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental) 19 | [![Travis build status](https://travis-ci.org/djnavarro/workbch.svg?branch=master)](https://travis-ci.org/djnavarro/workbch) 20 | [![CRAN status](https://www.r-pkg.org/badges/version/workbch)](https://cran.r-project.org/package=workbch) 21 | [![Codecov test coverage](https://codecov.io/gh/djnavarro/workbch/branch/master/graph/badge.svg)](https://codecov.io/gh/djnavarro/workbch?branch=master) 22 | 23 | 24 | The workbch ("work bench") package provides simple utility functions to help the R user keep track of projects and navigate between them. The package is designed around the concept of jobs, where a job might correspond to an RStudio project, a git repository, a research project or indeed all of the above. Jobs are assumed to be stored in a single folder, but can be associated with URLs (e.g., on GitHub, Overleaf, OSF, or elsewhere). The package is intended to be used interactively, though most functions can be called programmatically. 25 | 26 | ## Installation 27 | 28 | The workbch package has not been released on CRAN. You can install the 29 | development version from GitHub with: 30 | 31 | ``` r 32 | # install.packages("remotes") 33 | remotes::install_github("djnavarro/workbch") 34 | ``` 35 | 36 | 37 | ## Overview 38 | 39 | The package consists of nine functions. Three functions are used to create, modify, and delete jobs 40 | 41 | - `job_create()`. Create a new job 42 | - `job_modify()`. Modify or delete an existing job 43 | - `job_seek()`. Scans a directory recursively to find jobs 44 | 45 | There are three functions that are useful for navigation: 46 | 47 | - `job_open()`. Opens an RStudio project or changes working directory 48 | - `job_openurl()`. Opens a URL associated with a job in a browser window 49 | - `job_home()`. Returns the path to the job folder 50 | 51 | There are three functions that are useful for keeping track of projects: 52 | 53 | - `job_gitreport()`. Shows the git status of all jobs 54 | - `job_glimpse()`. Shows all information stored about a job 55 | - `job_list()`. Displays a table summarising all jobs, or a subset of them. 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # workbch 5 | 6 | 7 | 8 | [![Lifecycle: 9 | experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental) 10 | [![Travis build 11 | status](https://travis-ci.org/djnavarro/workbch.svg?branch=master)](https://travis-ci.org/djnavarro/workbch) 12 | [![CRAN 13 | status](https://www.r-pkg.org/badges/version/workbch)](https://cran.r-project.org/package=workbch) 14 | [![Codecov test 15 | coverage](https://codecov.io/gh/djnavarro/workbch/branch/master/graph/badge.svg)](https://codecov.io/gh/djnavarro/workbch?branch=master) 16 | 17 | 18 | The workbch (“work bench”) package provides simple utility functions to 19 | help the R user keep track of projects and navigate between them. The 20 | package is designed around the concept of jobs, where a job might 21 | correspond to an RStudio project, a git repository, a research project 22 | or indeed all of the above. Jobs are assumed to be stored in a single 23 | folder, but can be associated with URLs (e.g., on GitHub, Overleaf, OSF, 24 | or elsewhere). The package is intended to be used interactively, though 25 | most functions can be called programmatically. 26 | 27 | ## Installation 28 | 29 | The workbch package has not been released on CRAN. You can install the 30 | development version from GitHub with: 31 | 32 | ``` r 33 | # install.packages("remotes") 34 | remotes::install_github("djnavarro/workbch") 35 | ``` 36 | 37 | ## Overview 38 | 39 | The package consists of nine functions. Three functions are used to 40 | create, modify, and delete jobs 41 | 42 | - `job_create()`. Create a new job 43 | - `job_modify()`. Modify or delete an existing job 44 | - `job_seek()`. Scans a directory recursively to find jobs 45 | 46 | There are three functions that are useful for navigation: 47 | 48 | - `job_open()`. Opens an RStudio project or changes working directory 49 | - `job_openurl()`. Opens a URL associated with a job in a browser 50 | window 51 | - `job_home()`. Returns the path to the job folder 52 | 53 | There are three functions that are useful for keeping track of projects: 54 | 55 | - `job_gitreport()`. Shows the git status of all jobs 56 | - `job_glimpse()`. Shows all information stored about a job 57 | - `job_list()`. Displays a table summarising all jobs, or a subset of 58 | them. 59 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | destination: docs 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 1% 13 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Page not found (404) • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 110 | 111 | 112 | 113 |
114 | 115 |
116 |
117 | 120 | 121 | Content not found. Please use links in the navbar. 122 | 123 |
124 | 125 |
126 | 127 | 128 | 129 | 139 |
140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | workbch.djnavarro.net -------------------------------------------------------------------------------- /docs/LICENSE-text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | License • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 110 | 111 | 112 | 113 |
114 | 115 |
116 |
117 | 120 | 121 |
YEAR: 2019
122 | COPYRIGHT HOLDER: Danielle Navarro
123 | 
124 | 125 |
126 | 127 |
128 | 129 | 130 | 131 | 141 |
142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /docs/LICENSE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | MIT License • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 110 | 111 | 112 | 113 |
114 | 115 |
116 |
117 | 120 | 121 |
122 | 123 |

Copyright (c) 2019 Danielle Navarro

124 |

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

125 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

126 |

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

127 |
128 | 129 |
130 | 131 |
132 | 133 | 134 | 135 | 145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /docs/articles/gitrep.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/gitrep.gif -------------------------------------------------------------------------------- /docs/articles/gitrep.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/gitrep.mp4 -------------------------------------------------------------------------------- /docs/articles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Articles • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 110 | 111 | 112 | 113 |
114 | 115 |
116 |
117 | 120 | 121 |
122 |

All vignettes

123 |

124 | 125 | 129 |
130 |
131 |
132 | 133 | 134 | 144 |
145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /docs/articles/modify.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/modify.gif -------------------------------------------------------------------------------- /docs/articles/modify.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/modify.mp4 -------------------------------------------------------------------------------- /docs/articles/moving.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/moving.gif -------------------------------------------------------------------------------- /docs/articles/moving.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/moving.mp4 -------------------------------------------------------------------------------- /docs/articles/navigation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/navigation.gif -------------------------------------------------------------------------------- /docs/articles/navigation.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/navigation.mp4 -------------------------------------------------------------------------------- /docs/articles/setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/setup.gif -------------------------------------------------------------------------------- /docs/articles/setup.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/docs/articles/setup.mp4 -------------------------------------------------------------------------------- /docs/articles/usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Using the workbch package • workbch 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 |
23 |
78 | 79 | 80 | 81 | 82 |
83 |
84 | 92 | 93 | 94 | 95 |
library(workbch)
96 | 102 |
103 |

104 | Git report

105 |

A second function of workbch is to keep track of the git status of various projects. A common workflow problem (for me, at least) is that I sometimes forget to commit changes or push them upstream, and then forget which projects I’ve left in this “unclean” state. The job_gitreport() function aims to make this easier by checking the git status for all jobs, and reporting those jobs that are not in a clean state. For example:

106 | 107 |
# A tibble: 3 x 6
108 |   jobname     staged unstaged untracked ahead behind
109 |   <chr>        <int>    <int>     <int> <int>  <int>
110 | 1 conjunction      0        1         1     3      0
111 | 2 psychtheory      0        0         1     0      0
112 | 3 tidylsrbook      0       24        12     0      0
113 |

Screencast illustrating how job_gitreport() works:

114 | 115 |
116 |
117 |

118 | Moving jobs

119 |

A third function of the workbch package is keeping track of job locations when they move around on the local machine, as long as they stay somewhere on the workbch search path (i.e., the “workbch.search” option). It does this by matching information stored in the “.workbch” sentinel file to entries in the “workbch_jobs.json” file. There are two functions that are useful for this:

120 |
    121 |
  • 122 | job_list() throws a warning if a job folder has moved
  • 123 |
  • 124 | job_seek() will ask if you want to update the location
  • 125 |
126 |

This is illustrated in the screencast below:

127 | 128 |
129 |
130 |

131 | Modifying jobs

132 |

A common task is to update the information associated with a job. The priority may change, it may be assigned a new owner or acquire a new URL, etc. The job_modify() function can be used for this, as illustrated below:

133 | 134 |
135 |
136 | 137 | 149 | 150 |
151 | 152 | 153 | 154 | 163 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /docs/authors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Authors • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 110 | 111 | 112 | 113 |
114 | 115 |
116 |
117 | 120 | 121 |
    122 |
  • 123 |

    Danielle Navarro. Author, maintainer. ORCID 124 |

    125 |
  • 126 |
127 | 128 |
129 | 130 |
131 | 132 | 133 | 134 | 144 |
145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /docs/docsearch.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | // register a handler to move the focus to the search bar 4 | // upon pressing shift + "/" (i.e. "?") 5 | $(document).on('keydown', function(e) { 6 | if (e.shiftKey && e.keyCode == 191) { 7 | e.preventDefault(); 8 | $("#search-input").focus(); 9 | } 10 | }); 11 | 12 | $(document).ready(function() { 13 | // do keyword highlighting 14 | /* modified from https://jsfiddle.net/julmot/bL6bb5oo/ */ 15 | var mark = function() { 16 | 17 | var referrer = document.URL ; 18 | var paramKey = "q" ; 19 | 20 | if (referrer.indexOf("?") !== -1) { 21 | var qs = referrer.substr(referrer.indexOf('?') + 1); 22 | var qs_noanchor = qs.split('#')[0]; 23 | var qsa = qs_noanchor.split('&'); 24 | var keyword = ""; 25 | 26 | for (var i = 0; i < qsa.length; i++) { 27 | var currentParam = qsa[i].split('='); 28 | 29 | if (currentParam.length !== 2) { 30 | continue; 31 | } 32 | 33 | if (currentParam[0] == paramKey) { 34 | keyword = decodeURIComponent(currentParam[1].replace(/\+/g, "%20")); 35 | } 36 | } 37 | 38 | if (keyword !== "") { 39 | $(".contents").unmark({ 40 | done: function() { 41 | $(".contents").mark(keyword); 42 | } 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | mark(); 49 | }); 50 | }); 51 | 52 | /* Search term highlighting ------------------------------*/ 53 | 54 | function matchedWords(hit) { 55 | var words = []; 56 | 57 | var hierarchy = hit._highlightResult.hierarchy; 58 | // loop to fetch from lvl0, lvl1, etc. 59 | for (var idx in hierarchy) { 60 | words = words.concat(hierarchy[idx].matchedWords); 61 | } 62 | 63 | var content = hit._highlightResult.content; 64 | if (content) { 65 | words = words.concat(content.matchedWords); 66 | } 67 | 68 | // return unique words 69 | var words_uniq = [...new Set(words)]; 70 | return words_uniq; 71 | } 72 | 73 | function updateHitURL(hit) { 74 | 75 | var words = matchedWords(hit); 76 | var url = ""; 77 | 78 | if (hit.anchor) { 79 | url = hit.url_without_anchor + '?q=' + escape(words.join(" ")) + '#' + hit.anchor; 80 | } else { 81 | url = hit.url + '?q=' + escape(words.join(" ")); 82 | } 83 | 84 | return url; 85 | } 86 | -------------------------------------------------------------------------------- /docs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /docs/pkgdown.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer */ 2 | 3 | /** 4 | * Basic idea: https://philipwalton.github.io/solved-by-flexbox/demos/sticky-footer/ 5 | * Details: https://github.com/philipwalton/solved-by-flexbox/blob/master/assets/css/components/site.css 6 | * 7 | * .Site -> body > .container 8 | * .Site-content -> body > .container .row 9 | * .footer -> footer 10 | * 11 | * Key idea seems to be to ensure that .container and __all its parents__ 12 | * have height set to 100% 13 | * 14 | */ 15 | 16 | html, body { 17 | height: 100%; 18 | } 19 | 20 | body > .container { 21 | display: flex; 22 | height: 100%; 23 | flex-direction: column; 24 | 25 | padding-top: 60px; 26 | } 27 | 28 | body > .container .row { 29 | flex: 1 0 auto; 30 | } 31 | 32 | footer { 33 | margin-top: 45px; 34 | padding: 35px 0 36px; 35 | border-top: 1px solid #e5e5e5; 36 | color: #666; 37 | display: flex; 38 | flex-shrink: 0; 39 | } 40 | footer p { 41 | margin-bottom: 0; 42 | } 43 | footer div { 44 | flex: 1; 45 | } 46 | footer .pkgdown { 47 | text-align: right; 48 | } 49 | footer p { 50 | margin-bottom: 0; 51 | } 52 | 53 | img.icon { 54 | float: right; 55 | } 56 | 57 | img { 58 | max-width: 100%; 59 | } 60 | 61 | /* Fix bug in bootstrap (only seen in firefox) */ 62 | summary { 63 | display: list-item; 64 | } 65 | 66 | /* Typographic tweaking ---------------------------------*/ 67 | 68 | .contents .page-header { 69 | margin-top: calc(-60px + 1em); 70 | } 71 | 72 | /* Section anchors ---------------------------------*/ 73 | 74 | a.anchor { 75 | margin-left: -30px; 76 | display:inline-block; 77 | width: 30px; 78 | height: 30px; 79 | visibility: hidden; 80 | 81 | background-image: url(./link.svg); 82 | background-repeat: no-repeat; 83 | background-size: 20px 20px; 84 | background-position: center center; 85 | } 86 | 87 | .hasAnchor:hover a.anchor { 88 | visibility: visible; 89 | } 90 | 91 | @media (max-width: 767px) { 92 | .hasAnchor:hover a.anchor { 93 | visibility: hidden; 94 | } 95 | } 96 | 97 | 98 | /* Fixes for fixed navbar --------------------------*/ 99 | 100 | .contents h1, .contents h2, .contents h3, .contents h4 { 101 | padding-top: 60px; 102 | margin-top: -40px; 103 | } 104 | 105 | /* Sidebar --------------------------*/ 106 | 107 | #sidebar { 108 | margin-top: 30px; 109 | position: -webkit-sticky; 110 | position: sticky; 111 | top: 70px; 112 | } 113 | #sidebar h2 { 114 | font-size: 1.5em; 115 | margin-top: 1em; 116 | } 117 | 118 | #sidebar h2:first-child { 119 | margin-top: 0; 120 | } 121 | 122 | #sidebar .list-unstyled li { 123 | margin-bottom: 0.5em; 124 | } 125 | 126 | .orcid { 127 | height: 16px; 128 | /* margins are required by official ORCID trademark and display guidelines */ 129 | margin-left:4px; 130 | margin-right:4px; 131 | vertical-align: middle; 132 | } 133 | 134 | /* Reference index & topics ----------------------------------------------- */ 135 | 136 | .ref-index th {font-weight: normal;} 137 | 138 | .ref-index td {vertical-align: top;} 139 | .ref-index .icon {width: 40px;} 140 | .ref-index .alias {width: 40%;} 141 | .ref-index-icons .alias {width: calc(40% - 40px);} 142 | .ref-index .title {width: 60%;} 143 | 144 | .ref-arguments th {text-align: right; padding-right: 10px;} 145 | .ref-arguments th, .ref-arguments td {vertical-align: top;} 146 | .ref-arguments .name {width: 20%;} 147 | .ref-arguments .desc {width: 80%;} 148 | 149 | /* Nice scrolling for wide elements --------------------------------------- */ 150 | 151 | table { 152 | display: block; 153 | overflow: auto; 154 | } 155 | 156 | /* Syntax highlighting ---------------------------------------------------- */ 157 | 158 | pre { 159 | word-wrap: normal; 160 | word-break: normal; 161 | border: 1px solid #eee; 162 | } 163 | 164 | pre, code { 165 | background-color: #f8f8f8; 166 | color: #333; 167 | } 168 | 169 | pre code { 170 | overflow: auto; 171 | word-wrap: normal; 172 | white-space: pre; 173 | } 174 | 175 | pre .img { 176 | margin: 5px 0; 177 | } 178 | 179 | pre .img img { 180 | background-color: #fff; 181 | display: block; 182 | height: auto; 183 | } 184 | 185 | code a, pre a { 186 | color: #375f84; 187 | } 188 | 189 | a.sourceLine:hover { 190 | text-decoration: none; 191 | } 192 | 193 | .fl {color: #1514b5;} 194 | .fu {color: #000000;} /* function */ 195 | .ch,.st {color: #036a07;} /* string */ 196 | .kw {color: #264D66;} /* keyword */ 197 | .co {color: #888888;} /* comment */ 198 | 199 | .message { color: black; font-weight: bolder;} 200 | .error { color: orange; font-weight: bolder;} 201 | .warning { color: #6A0366; font-weight: bolder;} 202 | 203 | /* Clipboard --------------------------*/ 204 | 205 | .hasCopyButton { 206 | position: relative; 207 | } 208 | 209 | .btn-copy-ex { 210 | position: absolute; 211 | right: 0; 212 | top: 0; 213 | visibility: hidden; 214 | } 215 | 216 | .hasCopyButton:hover button.btn-copy-ex { 217 | visibility: visible; 218 | } 219 | 220 | /* headroom.js ------------------------ */ 221 | 222 | .headroom { 223 | will-change: transform; 224 | transition: transform 200ms linear; 225 | } 226 | .headroom--pinned { 227 | transform: translateY(0%); 228 | } 229 | .headroom--unpinned { 230 | transform: translateY(-100%); 231 | } 232 | 233 | /* mark.js ----------------------------*/ 234 | 235 | mark { 236 | background-color: rgba(255, 255, 51, 0.5); 237 | border-bottom: 2px solid rgba(255, 153, 51, 0.3); 238 | padding: 1px; 239 | } 240 | 241 | /* vertical spacing after htmlwidgets */ 242 | .html-widget { 243 | margin-bottom: 10px; 244 | } 245 | 246 | /* fontawesome ------------------------ */ 247 | 248 | .fab { 249 | font-family: "Font Awesome 5 Brands" !important; 250 | } 251 | -------------------------------------------------------------------------------- /docs/pkgdown.js: -------------------------------------------------------------------------------- 1 | /* http://gregfranko.com/blog/jquery-best-practices/ */ 2 | (function($) { 3 | $(function() { 4 | 5 | $('.navbar-fixed-top').headroom(); 6 | 7 | $('body').scrollspy({ 8 | target: '#sidebar', 9 | offset: 60 10 | }); 11 | 12 | $('[data-toggle="tooltip"]').tooltip(); 13 | 14 | var cur_path = paths(location.pathname); 15 | var links = $("#navbar ul li a"); 16 | var max_length = -1; 17 | var pos = -1; 18 | for (var i = 0; i < links.length; i++) { 19 | if (links[i].getAttribute("href") === "#") 20 | continue; 21 | // Ignore external links 22 | if (links[i].host !== location.host) 23 | continue; 24 | 25 | var nav_path = paths(links[i].pathname); 26 | 27 | var length = prefix_length(nav_path, cur_path); 28 | if (length > max_length) { 29 | max_length = length; 30 | pos = i; 31 | } 32 | } 33 | 34 | // Add class to parent
  • , and enclosing
  • if in dropdown 35 | if (pos >= 0) { 36 | var menu_anchor = $(links[pos]); 37 | menu_anchor.parent().addClass("active"); 38 | menu_anchor.closest("li.dropdown").addClass("active"); 39 | } 40 | }); 41 | 42 | function paths(pathname) { 43 | var pieces = pathname.split("/"); 44 | pieces.shift(); // always starts with / 45 | 46 | var end = pieces[pieces.length - 1]; 47 | if (end === "index.html" || end === "") 48 | pieces.pop(); 49 | return(pieces); 50 | } 51 | 52 | // Returns -1 if not found 53 | function prefix_length(needle, haystack) { 54 | if (needle.length > haystack.length) 55 | return(-1); 56 | 57 | // Special case for length-0 haystack, since for loop won't run 58 | if (haystack.length === 0) { 59 | return(needle.length === 0 ? 0 : -1); 60 | } 61 | 62 | for (var i = 0; i < haystack.length; i++) { 63 | if (needle[i] != haystack[i]) 64 | return(i); 65 | } 66 | 67 | return(haystack.length); 68 | } 69 | 70 | /* Clipboard --------------------------*/ 71 | 72 | function changeTooltipMessage(element, msg) { 73 | var tooltipOriginalTitle=element.getAttribute('data-original-title'); 74 | element.setAttribute('data-original-title', msg); 75 | $(element).tooltip('show'); 76 | element.setAttribute('data-original-title', tooltipOriginalTitle); 77 | } 78 | 79 | if(ClipboardJS.isSupported()) { 80 | $(document).ready(function() { 81 | var copyButton = ""; 82 | 83 | $(".examples, div.sourceCode").addClass("hasCopyButton"); 84 | 85 | // Insert copy buttons: 86 | $(copyButton).prependTo(".hasCopyButton"); 87 | 88 | // Initialize tooltips: 89 | $('.btn-copy-ex').tooltip({container: 'body'}); 90 | 91 | // Initialize clipboard: 92 | var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', { 93 | text: function(trigger) { 94 | return trigger.parentNode.textContent; 95 | } 96 | }); 97 | 98 | clipboardBtnCopies.on('success', function(e) { 99 | changeTooltipMessage(e.trigger, 'Copied!'); 100 | e.clearSelection(); 101 | }); 102 | 103 | clipboardBtnCopies.on('error', function() { 104 | changeTooltipMessage(e.trigger,'Press Ctrl+C or Command+C to copy'); 105 | }); 106 | }); 107 | } 108 | })(window.jQuery || window.$) 109 | -------------------------------------------------------------------------------- /docs/pkgdown.yml: -------------------------------------------------------------------------------- 1 | pandoc: 2.3.1 2 | pkgdown: 1.3.0.9100 3 | pkgdown_sha: 75c3954d45c73f31750ae14fb08502d86adf3e2b 4 | articles: 5 | setup: setup.html 6 | usage: usage.html 7 | 8 | -------------------------------------------------------------------------------- /docs/reference/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Function reference • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
    54 |
    55 | 110 | 111 | 112 | 113 |
    114 | 115 |
    116 |
    117 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 135 | 136 | 137 | 138 | 141 | 142 | 143 | 144 | 147 | 148 | 149 | 150 | 153 | 154 | 155 | 156 | 159 | 160 | 161 | 162 | 165 | 166 | 167 | 168 | 171 | 172 | 173 | 174 | 177 | 178 | 179 | 180 | 183 | 184 | 185 | 186 | 189 | 190 | 191 | 192 |
    132 |

    All functions

    133 |

    134 |
    139 |

    job_create()

    140 |

    Create a new job

    145 |

    job_gitreport()

    146 |

    Report the git status of all jobs

    151 |

    job_glimpse()

    152 |

    View the information stored about a job

    157 |

    job_home()

    158 |

    Path to the job home

    163 |

    job_list()

    164 |

    View a list of all jobs

    169 |

    job_modify()

    170 |

    Modify the properties of a job

    175 |

    job_open()

    176 |

    Navigate to a job

    181 |

    job_openurl()

    182 |

    Navigate to a URL associated with a job

    187 |

    job_seek()

    188 |

    Scans for jobs

    193 |
    194 | 195 | 201 |
    202 | 203 | 204 |
    205 | 208 | 209 |
    210 |

    Site built with pkgdown 1.3.0.9100.

    211 |
    212 | 213 |
    214 |
    215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /docs/reference/job_gitreport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Report the git status of all jobs — job_gitreport • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    Report the git status of all jobs

    129 | 130 |
    131 | 132 |
    job_gitreport(show_clean = FALSE)
    133 | 134 |

    Arguments

    135 | 136 | 137 | 138 | 139 | 140 | 141 |
    show_clean

    should clean repositories be included in the output?

    142 | 143 |

    Value

    144 | 145 |

    A tibble with columns jobname, staged, unstaged, 146 | untracked, ahead and behind. The jobname column is 147 | a character vector, all others are integer valued.

    148 | 149 |

    Details

    150 | 151 |

    The role of the git_report() function is to provide an overview 152 | of the status of all workbch jobs that are associated with a git repository. 153 | For every job, it uses git2r::in_repository to determine if the job 154 | folder (i.e., the path for that job) is in a git repository. Jobs that are 155 | not in git repositories are ignored.

    156 |

    For jobs that are associated with git repositories, the git_report() 157 | function calls git2r::status() to determine the git status. If there is an 158 | upstream set (i.e., git2r::branch_get_upstream() detects an upstream 159 | repository), it will also call git2r::ahead_behind() to determine how many 160 | commits the local repository is ahead and/or behind of the upstream.

    161 |

    By default, no output is shown for clean repositories (show_clean = FALSE). 162 | A repository is deemed to be clean if there are no staged, unstaged or 163 | untracked files and it is neither ahead nor behind the upstream repository. 164 | If the user specifies show_clean = TRUE, then results are reported for every 165 | job that is linked to a git repository.

    166 | 167 | 168 |
    169 | 180 |
    181 | 182 | 183 |
    184 | 187 | 188 |
    189 |

    Site built with pkgdown 1.3.0.9100.

    190 |
    191 | 192 |
    193 |
    194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /docs/reference/job_glimpse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | View the information stored about a job — job_glimpse • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    View the information stored about a job

    129 | 130 |
    131 | 132 |
    job_glimpse(jobname = NULL)
    133 | 134 |

    Arguments

    135 | 136 | 137 | 138 | 139 | 140 | 141 |
    jobname

    Name of the job to show (defaults to the current job)

    142 | 143 |

    Value

    144 | 145 |

    Invisibly returns the job as a list

    146 | 147 |

    Details

    148 | 149 |

    The job_glimpse() function displays parameter values for the 150 | job specified in the jobname argument. If no argument is specified 151 | job_glimpse() will attempt to guess the "current" job by looking at 152 | any open RStudio projects. If no project is open (or the RStudio API is not 153 | available) it attempts to guess by looking at the working directory. The 154 | output is presented to the user as a message.

    155 | 156 | 157 |
    158 | 169 |
    170 | 171 | 172 |
    173 | 176 | 177 |
    178 |

    Site built with pkgdown 1.3.0.9100.

    179 |
    180 | 181 |
    182 |
    183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /docs/reference/job_home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Path to the job home — job_home • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    Path to the job home

    129 | 130 |
    131 | 132 |
    job_home(jobname = NULL)
    133 | 134 |

    Arguments

    135 | 136 | 137 | 138 | 139 | 140 | 141 |
    jobname

    Name of job (defaults to current job)

    142 | 143 |

    Value

    144 | 145 |

    Path to the job folder as a character string

    146 | 147 |

    Details

    148 | 149 |

    The job_home() function returns the file path for `jobname`. 150 | This can be useful if you need to specify the location of files relative to 151 | a project.

    152 |

    If no argument is specified job_home() will attempt to guess the 153 | current job by looking at any open RStudio projects. If no project is open 154 | or the RStudio API is not available it attempts to guess by looking at the 155 | working directory.

    156 | 157 | 158 |
    159 | 170 |
    171 | 172 | 173 |
    174 | 177 | 178 |
    179 |

    Site built with pkgdown 1.3.0.9100.

    180 |
    181 | 182 |
    183 |
    184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/reference/job_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | View a list of all jobs — job_list • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    View a list of all jobs

    129 | 130 |
    131 | 132 |
    job_list(search = NULL, select = c("jobname", "owner", "priority",
    133 |   "status", "description"))
    134 | 135 |

    Arguments

    136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
    search

    input query used to extract a subset of jobs

    select

    character vector of columns to return

    147 | 148 |

    Value

    149 | 150 |

    An object of class worbch_tbl. It is regular tibble with a slightly 151 | modified print method that ensures all rows are printed in the output

    152 | 153 |

    Details

    154 | 155 |

    The job_list() function is used to display a summary of the 156 | jobs known to workbch, shown as a tibble with one row per job. By default 157 | only those jobs with a priority value of 1 or 2 will be shown, and 158 | only if their status is "active" or "inactive". Jobs that do not have 159 | sufficient priority or whose status is "complete", "abandoned" or "masked" 160 | are not shown.

    161 |

    The default behaviour can be overridden by specifying a search string (or 162 | object that can be coerced to a string). If the user specifies a value to 163 | search then job_list() attempts to match the search string with a parameter 164 | value. For instance job_list("active") will return all jobs that have status 165 | active and job_list(1) will return all jobs that have priority 1. At 166 | present this functionality requires that the search string be an exact value 167 | (e.g., search = "Dani" would not match a job owned by "Danielle"), 168 | though this may be extended in the future.

    169 |

    The output is returned to the user as a tibble, and by default the columns 170 | displayed are jobname, owner, priority, status, 171 | and description. The user can directly specify which columns to be 172 | included using the select argument, which should be a character vector 173 | specifying the names of the columns to include. In addition to the five 174 | columns made available by default, the output can include the path, 175 | tags and urls associated with the job. For convenience, if you 176 | set select = NULL then all columns will be returned.

    177 | 178 | 179 |
    180 | 191 |
    192 | 193 | 194 |
    195 | 198 | 199 |
    200 |

    Site built with pkgdown 1.3.0.9100.

    201 |
    202 | 203 |
    204 |
    205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /docs/reference/job_open.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Navigate to a job — job_open • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    Navigate to a job

    129 | 130 |
    131 | 132 |
    job_open(jobname = NULL)
    133 | 134 |

    Arguments

    135 | 136 | 137 | 138 | 139 | 140 | 141 |
    jobname

    name of job to open

    142 | 143 |

    Details

    144 | 145 |

    The job_open() function opens a workbch job. The meaning of 146 | "open" here depends on context. If there is an RStudio project linked to the 147 | job and the RStudio API is available, then job_open() will switch the 148 | to the RStudio project that jobname is linked to. If this is not 149 | possible an there is a path associated with the job, then job_open() 150 | will change the working directory to that path. If neither option is possible 151 | then this function does nothing.

    152 |

    Note that when working interactively, job_open() can be called without 153 | specfying the jobname. In this case the user will be presented with 154 | prompts to select the desired job.

    155 | 156 | 157 |
    158 | 167 |
    168 | 169 | 170 |
    171 | 174 | 175 |
    176 |

    Site built with pkgdown 1.3.0.9100.

    177 |
    178 | 179 |
    180 |
    181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /docs/reference/job_openurl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Navigate to a URL associated with a job — job_openurl • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    Navigate to a URL associated with a job

    129 | 130 |
    131 | 132 |
    job_openurl(site, jobname = NULL)
    133 | 134 |

    Arguments

    135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |
    site

    label denoting the site (e.g., "github")

    jobname

    name of the job

    146 | 147 |

    Details

    148 | 149 |

    The job_openurl() function opens a URL associated with a 150 | job in a browser window. The site argument is the site nickname 151 | (e.g. "github", "homepage", etc) and the jobname argument specifies 152 | the job to use.

    153 |

    If no argument is specified job_openurl() will attempt to guess the 154 | current job by looking at any open RStudio projects. If no project is open 155 | or the RStudio API is not available it attempts to guess by looking at the 156 | working directory.

    157 | 158 | 159 |
    160 | 169 |
    170 | 171 | 172 |
    173 | 176 | 177 |
    178 |

    Site built with pkgdown 1.3.0.9100.

    179 |
    180 | 181 |
    182 |
    183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /docs/reference/job_seek.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Scans for jobs — job_seek • workbch 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | 113 | 114 | 115 | 116 |
    117 | 118 |
    119 |
    120 | 125 | 126 |
    127 | 128 |

    Scans for jobs

    129 | 130 |
    131 | 132 |
    job_seek(dirs = getOption("workbch.search"), seek = c("workbch", "git",
    133 |   "Rproj"), nesting = FALSE, owner = "", priority = 1,
    134 |   status = "active", tags = character(0))
    135 | 136 |

    Arguments

    137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
    dirs

    directories to scan

    seek

    search criterion

    nesting

    allow nested jobs (default = FALSE)

    owner

    default owner for a created job

    priority

    default priority for a created job

    status

    default status for a created job

    tags

    default tags for a created job

    168 | 169 |

    Details

    170 | 171 |

    The job_seek() searches the folders listed in dirs 172 | to find possible candidates that could be added as workbch jobs, or to 173 | recover the location of any such jobs that may have moved. When candidate 174 | folders are found, the user is presented with prompts asking if they would 175 | like to add the folder as a workbch job (or pair it with an existing job 176 | if a match is discovered).

    177 |

    The search process uses a heuristic to discover possible jobs. A folder 178 | is considered a possible candidate if it has a .Rproj file, a 179 | .workbch file, or a git repository. If nesting = FALSE (the 180 | default), a directory that is contained within another directory that meets 181 | one of these criteria is not considered a candidate.

    182 |

    As much as possible the job_seek() function attempts to guess values 183 | for the various parameters. For instance, if there is a git repository that 184 | has an upstream remote (on gitbub, bitbucket or gitlab), it attempts to guess 185 | an appropriate URL for the remote. Similarly, if there is a .workbch 186 | sentinel file, it uses the contents of the file to try to match the folder 187 | against a known job and takes the jobname from the sentinel file. Otherwise 188 | it guesses a default value using the folder name. 189 | For parameter values that cannot be guessed from the folder itself, default 190 | values are specified using the owner, priority, status 191 | and tags arguments.

    192 |

    Regardless of the values guessed, the user is prompted either to confirm 193 | the guessed value (by hitting enter) or overriding the guess by supplying 194 | the value manually.

    195 | 196 | 197 |
    198 | 207 |
    208 | 209 | 210 |
    211 | 214 | 215 |
    216 |

    Site built with pkgdown 1.3.0.9100.

    217 |
    218 | 219 |
    220 |
    221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /man/job_create.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_create.R 3 | \name{job_create} 4 | \alias{job_create} 5 | \title{Create a new job} 6 | \usage{ 7 | job_create(jobname = NULL, description = NULL, owner = NULL, 8 | status = NULL, priority = NULL, tags = NULL, path = NULL) 9 | } 10 | \arguments{ 11 | \item{jobname}{name of the job to create} 12 | 13 | \item{description}{brief description of the job} 14 | 15 | \item{owner}{should be a name or a nickname} 16 | 17 | \item{status}{should be "active", "inactive", "complete", "abandoned", "masked"} 18 | 19 | \item{priority}{numeric} 20 | 21 | \item{tags}{a string containing comma separated list of tags} 22 | 23 | \item{path}{path to the job home directory} 24 | } 25 | \value{ 26 | Invisibly returns a list containing the parameters for the job 27 | } 28 | \description{ 29 | Create a new job 30 | } 31 | \details{ 32 | The role of the \code{job_create()} function is to create new workbch job. 33 | It can be called in two ways, interactively or programmatically. To call the 34 | function interactively, R must be in interactive mode and the function should 35 | be called with no arguments specified. When called in this fashion the user 36 | will be presented with a sequence of prompts, asking them to specify each 37 | of the parameters that define a job (e.g., a character string for \code{jobname}, 38 | a number for \code{priority}). When used interactively, you do not need to include 39 | quote marks when entering a string: \code{job_create()} will coerce the input to 40 | the appropriate format, and then append the created job to the job file. 41 | 42 | When called programmatically, the user must specify the arguments in the 43 | call to \code{job_create()}. The \code{jobname}, \code{description} and 44 | \code{owner} arguments should be character strings of length 1, and all three 45 | are mandatory. The \code{status} for a job should be one of the following 46 | values: \code{"active"}, \code{"inactive"}, \code{"complete"}, \code{"abandoned"} 47 | or \code{"masked"}. The \code{priority} for a job should be a positive integer: 48 | the intent is that priority 1 is the highest priority, followed by priority 2, 49 | and so one. The \code{tags} for a job can be specified as a single string, using 50 | \code{|} as a separator character (e.g., \code{tags = "research | statistics"} 51 | would create two tags for the job). Finally, the \code{path} should specify the 52 | location of a folder containing the project files. 53 | 54 | For non-mandatory arguments, if the user does not specify a value, the 55 | \code{job_create()} function applies the following defaults: \code{priority = 1}, 56 | \code{status = "active"}, \code{tags = character(0)} and \code{path = NA}`. 57 | 58 | Note that, although jobs can also be associated with URLs (e.g., link to a 59 | GitHub repository or a document on Overleaf), the \code{job_create()} function 60 | does not (at this time) allow you to specify URLs. These can be added using 61 | \code{job_modify()}. 62 | } 63 | -------------------------------------------------------------------------------- /man/job_gitreport.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_gitreport.R 3 | \name{job_gitreport} 4 | \alias{job_gitreport} 5 | \title{Report the git status of all jobs} 6 | \usage{ 7 | job_gitreport(show_clean = FALSE) 8 | } 9 | \arguments{ 10 | \item{show_clean}{should clean repositories be included in the output?} 11 | } 12 | \value{ 13 | A tibble with columns \code{jobname}, \code{staged}, \code{unstaged}, 14 | \code{untracked}, \code{ahead} and \code{behind}. The \code{jobname} column is 15 | a character vector, all others are integer valued. 16 | } 17 | \description{ 18 | Report the git status of all jobs 19 | } 20 | \details{ 21 | The role of the \code{git_report()} function is to provide an overview 22 | of the status of all workbch jobs that are associated with a git repository. 23 | For every job, it uses \code{git2r::in_repository} to determine if the job 24 | folder (i.e., the \code{path} for that job) is in a git repository. Jobs that are 25 | not in git repositories are ignored. 26 | 27 | For jobs that are associated with git repositories, the \code{git_report()} 28 | function calls \code{git2r::status()} to determine the git status. If there is an 29 | upstream set (i.e., \code{git2r::branch_get_upstream()} detects an upstream 30 | repository), it will also call \code{git2r::ahead_behind()} to determine how many 31 | commits the local repository is ahead and/or behind of the upstream. 32 | 33 | By default, no output is shown for clean repositories (\code{show_clean = FALSE}). 34 | A repository is deemed to be clean if there are no staged, unstaged or 35 | untracked files and it is neither ahead nor behind the upstream repository. 36 | If the user specifies \code{show_clean = TRUE}, then results are reported for every 37 | job that is linked to a git repository. 38 | } 39 | -------------------------------------------------------------------------------- /man/job_glimpse.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_glimpse.R 3 | \name{job_glimpse} 4 | \alias{job_glimpse} 5 | \title{View the information stored about a job} 6 | \usage{ 7 | job_glimpse(jobname = NULL) 8 | } 9 | \arguments{ 10 | \item{jobname}{Name of the job to show (defaults to the current job)} 11 | } 12 | \value{ 13 | Invisibly returns the job as a list 14 | } 15 | \description{ 16 | View the information stored about a job 17 | } 18 | \details{ 19 | The \code{job_glimpse()} function displays parameter values for the 20 | job specified in the \code{jobname} argument. If no argument is specified 21 | \code{job_glimpse()} will attempt to guess the "current" job by looking at 22 | any open RStudio projects. If no project is open (or the RStudio API is not 23 | available) it attempts to guess by looking at the working directory. The 24 | output is presented to the user as a message. 25 | } 26 | -------------------------------------------------------------------------------- /man/job_home.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_home.R 3 | \name{job_home} 4 | \alias{job_home} 5 | \title{Path to the job home} 6 | \usage{ 7 | job_home(jobname = NULL) 8 | } 9 | \arguments{ 10 | \item{jobname}{Name of job (defaults to current job)} 11 | } 12 | \value{ 13 | Path to the job folder as a character string 14 | } 15 | \description{ 16 | Path to the job home 17 | } 18 | \details{ 19 | The \code{job_home()} function returns the file path for `jobname`. 20 | This can be useful if you need to specify the location of files relative to 21 | a project. 22 | 23 | If no argument is specified \code{job_home()} will attempt to guess the 24 | current job by looking at any open RStudio projects. If no project is open 25 | or the RStudio API is not available it attempts to guess by looking at the 26 | working directory. 27 | } 28 | -------------------------------------------------------------------------------- /man/job_list.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_list.R 3 | \name{job_list} 4 | \alias{job_list} 5 | \title{View a list of all jobs} 6 | \usage{ 7 | job_list(search = NULL, select = c("jobname", "owner", "priority", 8 | "status", "description")) 9 | } 10 | \arguments{ 11 | \item{search}{input query used to extract a subset of jobs} 12 | 13 | \item{select}{character vector of columns to return} 14 | } 15 | \value{ 16 | An object of class worbch_tbl. It is regular tibble with a slightly 17 | modified print method that ensures all rows are printed in the output 18 | } 19 | \description{ 20 | View a list of all jobs 21 | } 22 | \details{ 23 | The \code{job_list()} function is used to display a summary of the 24 | jobs known to workbch, shown as a tibble with one row per job. By default 25 | only those jobs with a \code{priority} value of 1 or 2 will be shown, and 26 | only if their \code{status} is "active" or "inactive". Jobs that do not have 27 | sufficient priority or whose status is "complete", "abandoned" or "masked" 28 | are not shown. 29 | 30 | The default behaviour can be overridden by specifying a \code{search} string (or 31 | object that can be coerced to a string). If the user specifies a value to 32 | \code{search} then \code{job_list()} attempts to match the search string with a parameter 33 | value. For instance \code{job_list("active")} will return all jobs that have status 34 | \code{active} and \code{job_list(1)} will return all jobs that have priority 1. At 35 | present this functionality requires that the \code{search} string be an exact value 36 | (e.g., \code{search = "Dani"} would not match a job owned by \code{"Danielle"}), 37 | though this may be extended in the future. 38 | 39 | The output is returned to the user as a tibble, and by default the columns 40 | displayed are \code{jobname}, \code{owner}, \code{priority}, \code{status}, 41 | and \code{description}. The user can directly specify which columns to be 42 | included using the \code{select} argument, which should be a character vector 43 | specifying the names of the columns to include. In addition to the five 44 | columns made available by default, the output can include the \code{path}, 45 | \code{tags} and \code{urls} associated with the job. For convenience, if you 46 | set \code{select = NULL} then all columns will be returned. 47 | } 48 | -------------------------------------------------------------------------------- /man/job_modify.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_modify.R 3 | \name{job_modify} 4 | \alias{job_modify} 5 | \title{Modify the properties of a job} 6 | \usage{ 7 | job_modify(jobname = NULL, newname = NULL, description = NULL, 8 | owner = NULL, status = NULL, priority = NULL, path = NULL, 9 | tags = NULL, url = NULL, delete = FALSE) 10 | } 11 | \arguments{ 12 | \item{jobname}{the current name for the job} 13 | 14 | \item{newname}{the new name} 15 | 16 | \item{description}{the new description} 17 | 18 | \item{owner}{nick name for the new owner} 19 | 20 | \item{status}{the new status} 21 | 22 | \item{priority}{the new priority} 23 | 24 | \item{path}{the new path to the job folder} 25 | 26 | \item{tags}{string with tags (uses | as separator)} 27 | 28 | \item{url}{string specifying url} 29 | 30 | \item{delete}{should this job be deleted (default = FALSE)} 31 | } 32 | \description{ 33 | Modify the properties of a job 34 | } 35 | \details{ 36 | The role of the \code{job_modify()} function is to change one or more 37 | parameters of an existing workbch job. It can be called in two ways, interactively 38 | or programmatically. To call the function interactively, R must be in interactive 39 | mode and the function should be called specifying either the \code{jobname} argument 40 | only, or no arguments specified. If no \code{jobname} is specified the 41 | \code{job_modify()} function will attempt to guess the 42 | current job by looking at any open RStudio projects. If no project is open 43 | or the RStudio API is not available it attempts to guess by looking at the 44 | working directory 45 | 46 | When called interactiely, the user 47 | will be presented with a sequence of prompts, asking them to specify each 48 | of the parameters that define a job (e.g., a character string for \code{descripton}, 49 | a number for \code{priority}). When used interactively, you do not need to include 50 | quote marks when entering a string: \code{job_modify()} will coerce the input to 51 | the appropriate format, and then append the created job to the job file. 52 | 53 | When called programmatically, the user must specify the arguments in the 54 | call to \code{job_modify()}. Onlt the \code{jobname} argument is mandatory. 55 | To rename an existing job the \code{newname} argument should be specified. 56 | To modify \code{description}, \code{owner}, \code{status}, \code{priority}, 57 | \code{path}, \code{tags} or \code{urls} the relevant argument shoul be 58 | specified. 59 | 60 | The \code{status} of a job should be \code{"active"}, \code{"inactive"}, 61 | \code{"complete"}, \code{"abandoned"} or \code{"masked"}. The \code{priority} for 62 | a job should be a positive integer: the intent is that priority 1 is the highest 63 | priority, followed by priority 2, and so one. The \code{tags} for a job can be 64 | specified as a single string, using \code{|} as a separator character (e.g., 65 | \code{tags = "research | statistics"} would create two tags for the job). 66 | The \code{path} should specify the location of a folder containing the project 67 | files. The format for \code{urls} is the same as for tags. For example to 68 | specify a GitHub link one might write \code{urls = "github \ 69 | https://github.com/djnavarro/workbch"}. Multiple URLs can be specified by 70 | extending this syntax using \code{|} as the separator. For example: 71 | \code{urls = "github \ https://github.com/djnavarro/workbch | docs | 72 | https://djnavarro.github.io/workbch"}. 73 | 74 | Setting \code{delete = TRUE} will delete the job. 75 | } 76 | -------------------------------------------------------------------------------- /man/job_open.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_open.R 3 | \name{job_open} 4 | \alias{job_open} 5 | \title{Navigate to a job} 6 | \usage{ 7 | job_open(jobname = NULL) 8 | } 9 | \arguments{ 10 | \item{jobname}{name of job to open} 11 | } 12 | \description{ 13 | Navigate to a job 14 | } 15 | \details{ 16 | The \code{job_open()} function opens a workbch job. The meaning of 17 | "open" here depends on context. If there is an RStudio project linked to the 18 | job and the RStudio API is available, then \code{job_open()} will switch the 19 | to the RStudio project that \code{jobname} is linked to. If this is not 20 | possible an there is a path associated with the job, then \code{job_open()} 21 | will change the working directory to that path. If neither option is possible 22 | then this function does nothing. 23 | 24 | Note that when working interactively, \code{job_open()} can be called without 25 | specfying the \code{jobname}. In this case the user will be presented with 26 | prompts to select the desired job. 27 | } 28 | -------------------------------------------------------------------------------- /man/job_openurl.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_openurl.R 3 | \name{job_openurl} 4 | \alias{job_openurl} 5 | \title{Navigate to a URL associated with a job} 6 | \usage{ 7 | job_openurl(site, jobname = NULL) 8 | } 9 | \arguments{ 10 | \item{site}{label denoting the site (e.g., "github")} 11 | 12 | \item{jobname}{name of the job} 13 | } 14 | \description{ 15 | Navigate to a URL associated with a job 16 | } 17 | \details{ 18 | The \code{job_openurl()} function opens a URL associated with a 19 | job in a browser window. The \code{site} argument is the site nickname 20 | (e.g. "github", "homepage", etc) and the \code{jobname} argument specifies 21 | the job to use. 22 | 23 | If no argument is specified \code{job_openurl()} will attempt to guess the 24 | current job by looking at any open RStudio projects. If no project is open 25 | or the RStudio API is not available it attempts to guess by looking at the 26 | working directory. 27 | } 28 | -------------------------------------------------------------------------------- /man/job_seek.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/job_seek.R 3 | \name{job_seek} 4 | \alias{job_seek} 5 | \title{Scans for jobs} 6 | \usage{ 7 | job_seek(dirs = getOption("workbch.search"), seek = c("workbch", "git", 8 | "Rproj"), nesting = FALSE, owner = "", priority = 1, 9 | status = "active", tags = character(0)) 10 | } 11 | \arguments{ 12 | \item{dirs}{directories to scan} 13 | 14 | \item{seek}{search criterion} 15 | 16 | \item{nesting}{allow nested jobs (default = FALSE)} 17 | 18 | \item{owner}{default owner for a created job} 19 | 20 | \item{priority}{default priority for a created job} 21 | 22 | \item{status}{default status for a created job} 23 | 24 | \item{tags}{default tags for a created job} 25 | } 26 | \description{ 27 | Scans for jobs 28 | } 29 | \details{ 30 | The \code{job_seek()} searches the folders listed in \code{dirs} 31 | to find possible candidates that could be added as workbch jobs, or to 32 | recover the location of any such jobs that may have moved. When candidate 33 | folders are found, the user is presented with prompts asking if they would 34 | like to add the folder as a workbch job (or pair it with an existing job 35 | if a match is discovered). 36 | 37 | The search process uses a heuristic to discover possible jobs. A folder 38 | is considered a possible candidate if it has a \code{.Rproj} file, a 39 | \code{.workbch} file, or a git repository. If \code{nesting = FALSE} (the 40 | default), a directory that is contained within another directory that meets 41 | one of these criteria is not considered a candidate. 42 | 43 | As much as possible the \code{job_seek()} function attempts to guess values 44 | for the various parameters. For instance, if there is a git repository that 45 | has an upstream remote (on gitbub, bitbucket or gitlab), it attempts to guess 46 | an appropriate URL for the remote. Similarly, if there is a \code{.workbch} 47 | sentinel file, it uses the contents of the file to try to match the folder 48 | against a known job and takes the jobname from the sentinel file. Otherwise 49 | it guesses a default value using the folder name. 50 | For parameter values that cannot be guessed from the folder itself, default 51 | values are specified using the \code{owner}, \code{priority}, \code{status} 52 | and \code{tags} arguments. 53 | 54 | Regardless of the values guessed, the user is prompted either to confirm 55 | the guessed value (by hitting enter) or overriding the guess by supplying 56 | the value manually. 57 | } 58 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(workbch) 3 | 4 | wbh <- getOption("workbch.home") 5 | 6 | test_check("workbch") 7 | 8 | options(workbch.home = wbh) 9 | 10 | -------------------------------------------------------------------------------- /tests/testthat/test-job_create.R: -------------------------------------------------------------------------------- 1 | 2 | # set up 3 | loc <- tempdir() 4 | options(workbch.home = loc) 5 | 6 | # delete the job file if for some reason it exists 7 | if(file.exists(job_file())) { 8 | file.remove(job_file()) 9 | } 10 | 11 | # create path 12 | jobdir <- file.path(loc, "hitmebaby") 13 | jobdir2 <- file.path(loc, "slave4u") 14 | if(!dir.exists(jobdir)) dir.create(jobdir) 15 | sentinel <- file.path(jobdir, ".workbch") 16 | 17 | test_that("job_create works", { 18 | 19 | # create the job manually, no sentinel file, write nothing 20 | jobs <- list() 21 | jobs[["hitmebaby"]] <- new_job( 22 | jobname = "hitmebaby", 23 | description = "a song", 24 | owner = "britney", 25 | path = jobdir, 26 | sentinel = FALSE 27 | ) 28 | 29 | expect_false(file.exists(sentinel)) 30 | expect_false(file.exists(job_file())) 31 | 32 | # create the job 33 | job_create( 34 | jobname = "hitmebaby", 35 | description = "a song", 36 | owner = "britney", 37 | path = jobdir 38 | ) 39 | 40 | expect_true(file.exists(sentinel)) 41 | expect_true(file.exists(job_file())) 42 | 43 | # read the job file 44 | jobs2 <- job_read() 45 | jobs[["hitmebaby"]]$idstring <- jobs2[["hitmebaby"]]$idstring 46 | 47 | # expect equal in every respect except idstring 48 | expect_equal(jobs, jobs2) 49 | 50 | # expect errors when given insufficent input 51 | expect_error(job_create()) 52 | expect_error(job_create("hitmebaby")) 53 | expect_error(job_create("toxic")) 54 | expect_error(job_create("toxic", "a song")) 55 | 56 | # expect success when given minimal input and maximal input 57 | expect_silent(job_create("toxic", "a song", "britney")) 58 | expect_silent( 59 | job_create( 60 | jobname = "slave4u", 61 | description = "another song", 62 | owner = "britney", 63 | status = "active", 64 | priority = 3, 65 | tags = "aaa | bbb", 66 | path = jobdir2 67 | ) 68 | ) 69 | 70 | # expect that newly created jobs are appended 71 | jobs3 <- job_read() 72 | expect_length(jobs3, 3) 73 | expect_named(jobs3, c("hitmebaby", "toxic", "slave4u")) 74 | 75 | }) 76 | -------------------------------------------------------------------------------- /tests/testthat/test-job_glimpse.R: -------------------------------------------------------------------------------- 1 | # set up 2 | loc <- tempdir() 3 | options(workbch.home = loc) 4 | 5 | # delete the job file if for some reason it exists 6 | if(file.exists(job_file())) { 7 | file.remove(job_file()) 8 | } 9 | 10 | # create path 11 | jobdir <- file.path(loc, "hitmebaby") 12 | if(!dir.exists(jobdir)) dir.create(jobdir) 13 | 14 | # create a job 15 | job_create( 16 | jobname = "hitmebaby", 17 | description = "a song", 18 | owner = "britney", 19 | status = "masked", 20 | priority = 2, 21 | path = jobdir, 22 | tags = "awesome" 23 | ) 24 | jb <- job_read()[[1]] 25 | 26 | test_that("job_glimpse works", { 27 | 28 | # check that job_glimpse returns the job 29 | jb2 <- job_glimpse("hitmebaby") 30 | expect_equal(jb, jb2) 31 | 32 | # check that job_glimpse throws errors 33 | expect_error(job_glimpse(), "Could not detect current") 34 | expect_error(job_glimpse(1), "character and length 1") 35 | expect_error(job_glimpse("toxic"), "No job exists with") 36 | 37 | # check it defaults to current job if one exists 38 | wd <- setwd(jobdir) 39 | jb3 <- job_glimpse() 40 | expect_equal(jb, jb3) 41 | setwd(wd) 42 | 43 | # add a url and check 44 | job_modify("hitmebaby", url = "github | nopenope") 45 | jb4 <- job_glimpse("hitmebaby") 46 | expect_equal(job_read()[["hitmebaby"]], jb4) 47 | 48 | 49 | }) 50 | -------------------------------------------------------------------------------- /tests/testthat/test-job_home.R: -------------------------------------------------------------------------------- 1 | # set up 2 | loc <- tempdir() 3 | options(workbch.home = loc) 4 | 5 | # delete the job file if for some reason it exists 6 | if(file.exists(job_file())) { 7 | file.remove(job_file()) 8 | } 9 | 10 | # create path 11 | jobdir <- file.path(loc, "hitmebaby") 12 | if(!dir.exists(jobdir)) dir.create(jobdir) 13 | 14 | # create jobs, one with a path 15 | job_create( 16 | jobname = "hitmebaby", 17 | description = "a song", 18 | owner = "britney", 19 | path = jobdir 20 | ) 21 | job_create( 22 | jobname = "toxic", 23 | description = "a song", 24 | owner = "britney" 25 | ) 26 | 27 | test_that("job_home works", { 28 | 29 | # the three scenarios when a string is given 30 | expect_equal(job_home("hitmebaby"), jobdir) 31 | expect_error(job_home("toxic"), "No path known for") 32 | expect_error(job_home("dfasdfsdgdf"), "No job exists with") 33 | 34 | # make sure we're hitting verify_onestring 35 | expect_error(job_home(1), "must be character and length 1") 36 | 37 | # make sure we're hitting job_getcurrent 38 | expect_error(job_home(), "Could not detect current job") 39 | wd <-setwd(jobdir) 40 | expect_equal(job_home(), jobdir) 41 | setwd(wd) 42 | 43 | }) 44 | -------------------------------------------------------------------------------- /tests/testthat/test-job_list.R: -------------------------------------------------------------------------------- 1 | # set up 2 | loc <- tempdir() 3 | options(workbch.home = loc) 4 | 5 | # delete the job file if for some reason it exists 6 | if(file.exists(job_file())) { 7 | file.remove(job_file()) 8 | } 9 | 10 | # create path 11 | jobdir <- file.path(loc, "hitmebaby") 12 | if(!dir.exists(jobdir)) dir.create(jobdir) 13 | 14 | # create jobs 15 | job_create( 16 | jobname = "hitmebaby", 17 | description = "a song", 18 | owner = "britney", 19 | status = "inactive", 20 | priority = 2, 21 | path = jobdir, 22 | tags = "awesome" 23 | ) 24 | job_create( 25 | jobname = "toxic", 26 | description = "another song", 27 | owner = "britney", 28 | status = "active", 29 | priority = 1, 30 | tags = "awesome | fun" 31 | ) 32 | 33 | test_that("job_list works", { 34 | 35 | # make sure we have a jobs file 36 | expect_length(job_read(), 2) 37 | 38 | # by default it should return both jobs 39 | expect_equal(nrow(job_list()), 2) 40 | expect_named( 41 | job_list(), 42 | c("jobname", "owner", "priority", "status", "description") 43 | ) 44 | expect_equal(job_list()$jobname, c("toxic", "hitmebaby")) 45 | 46 | # now give some queries 47 | expect_equal(job_list("active")$jobname, "toxic") 48 | expect_equal(job_list(1)$jobname, "toxic") 49 | expect_null(job_list("masked")) 50 | expect_named(job_list(select = c("tags")), "tags") 51 | expect_named( 52 | job_list(select = NULL), 53 | c("jobname", "owner", "priority", "status", 54 | "description", "path", "tags", "urls") 55 | ) 56 | 57 | }) 58 | -------------------------------------------------------------------------------- /tests/testthat/test-job_modify.R: -------------------------------------------------------------------------------- 1 | # set up 2 | loc <- tempdir() 3 | options(workbch.home = loc) 4 | 5 | # delete the job file if for some reason it exists 6 | if(file.exists(job_file())) { 7 | file.remove(job_file()) 8 | } 9 | 10 | # create path 11 | jobdir <- file.path(loc, "hitmebaby") 12 | if(!dir.exists(jobdir)) dir.create(jobdir) 13 | 14 | # create a job object directly 15 | job1 <- list() 16 | job1[["hitmebaby"]] <- new_job( 17 | jobname = "hitmebaby", 18 | description = "a song", 19 | owner = "britney", 20 | status = "masked", 21 | priority = 2, 22 | path = jobdir, 23 | tags = "awesome", 24 | sentinel = FALSE 25 | ) 26 | 27 | # write a different entity to file 28 | job_create( 29 | jobname = "toxic", 30 | description = "another song", 31 | owner = "not britney" 32 | ) 33 | 34 | test_that("job_modify works", { 35 | 36 | # try modifying everything at once to transform toxic 37 | # into hitmebaby 38 | job_modify( 39 | jobname = "toxic", 40 | newname = "hitmebaby", 41 | description = "a song", 42 | owner = "britney", 43 | status = "masked", 44 | priority = 2, 45 | path = jobdir, 46 | tags = "awesome" 47 | ) 48 | 49 | # the idstring will be different, overwrite 50 | job2 <- job_read() 51 | job1[[1]]$idstring <- job2[[1]]$idstring 52 | 53 | # check identical 54 | expect_equal(job2, job1) 55 | 56 | # add a url 57 | job_modify( 58 | jobname = "hitmebaby", 59 | url = "github | http://nope.com" 60 | ) 61 | 62 | # check that the correct information is written 63 | job3 <- job_read() 64 | expect_equal(job3[["hitmebaby"]]$urls$site, "github") 65 | expect_equal(job3[["hitmebaby"]]$urls$link, "http://nope.com") 66 | 67 | # SOMETHING WEIRD HERE 68 | 69 | # # delete a url 70 | # job_modify( 71 | # jobname = "hitmebaby", 72 | # url = "github" 73 | # ) 74 | # 75 | # # chech that it is deleted 76 | # job3 <- job_read() 77 | # expect_length(job3[["hitmebaby"]]$urls$site, 0) 78 | # expect_length(job3[["hitmebaby"]]$urls$link, 0) 79 | 80 | 81 | # delete the job 82 | expect_equal(job_home("hitmebaby"), jobdir) 83 | job_modify("hitmebaby", delete = TRUE) 84 | expect_error(job_home("hitmebaby"), "No job exists with name") 85 | 86 | }) 87 | -------------------------------------------------------------------------------- /tests/testthat/test-miscellaneous.R: -------------------------------------------------------------------------------- 1 | test_that("empty_url works", { 2 | expect_equal( 3 | empty_url(), 4 | new_url( 5 | site = character(0), 6 | link = character(0), 7 | verify = FALSE 8 | ) 9 | ) 10 | }) 11 | 12 | test_that("idstring works", { 13 | set.seed(1) 14 | expect_equal(idstring(), "dMaHwQnrYG") 15 | }) 16 | 17 | test_that("print method works", { 18 | 19 | tbl1 <- tibble::tibble( 20 | x = rnorm(50), 21 | y = rnorm(50) 22 | ) 23 | 24 | tbl2 <- as_wkbch_tbl(tbl1) 25 | 26 | # same values different class 27 | expect_equivalent(tbl2, tbl1) 28 | expect_s3_class(tbl2, c("wkbch_tbl", class(tbl1))) 29 | 30 | # print method for wkbch_tbl fixes the output row 31 | out1 <- capture.output(print(tbl1, n = 100)) 32 | out2 <- capture.output(print(tbl2)) 33 | expect_identical(out1, out2) 34 | 35 | tbl3 <- tibble::tibble( 36 | x = rnorm(150), 37 | y = rnorm(150) 38 | ) 39 | 40 | tbl4 <- as_wkbch_tbl(tbl3) 41 | 42 | # print method for wkbch_tbl fixes the output row 43 | out3 <- capture.output(print(tbl3, n = 100)) 44 | out4 <- capture.output(print(tbl4)) 45 | expect_identical(out3, out4) 46 | 47 | 48 | }) 49 | 50 | test_that("split_url and split_tag work", { 51 | 52 | # make sure it calls verify_onestring 53 | expect_error(split_tags(1), "must be character and length 1") 54 | expect_error(split_url(1), "must be character and length 1") 55 | expect_error(split_tags(character(0)), "must be character and length 1") 56 | expect_error(split_url(character(0)), "must be character and length 1") 57 | expect_error(split_tags(c("a", "b")), "must be character and length 1") 58 | expect_error(split_url(c("a", "b")), "must be character and length 1") 59 | 60 | # make sure it splits 61 | expect_equal(split_tags(""), character(0)) 62 | expect_equal(split_tags("|"), character(0)) 63 | expect_equal(split_tags("aaa bbb"), "aaa bbb") 64 | expect_equal(split_tags("aaa | bbb"), c("aaa", "bbb")) 65 | expect_equal(split_tags(" aaa| "), "aaa") 66 | expect_equal(split_tags(" |bbb| "), "bbb") 67 | expect_equal(split_tags(" |aaa|||| bbb "), c("aaa", "bbb")) 68 | 69 | # make sure it splits 70 | expect_equal(split_url(""), character(0)) 71 | expect_equal(split_url("|"), character(0)) 72 | expect_equal(split_url("aaa bbb"), "aaa bbb") 73 | expect_equal(split_url("aaa | bbb"), c("aaa", "bbb")) 74 | expect_equal(split_url(" aaa| "), "aaa") 75 | expect_equal(split_url(" |bbb| "), "bbb") 76 | expect_equal(split_url(" |aaa|||| bbb "), c("aaa", "bbb")) 77 | 78 | 79 | }) 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /tests/testthat/test-pull.R: -------------------------------------------------------------------------------- 1 | 2 | # reset home to a tempory directory 3 | loc <- tempdir() 4 | options(workbch.home = loc) 5 | 6 | test_that("pull functions work", { 7 | 8 | jobs <- NULL 9 | 10 | # pull from empty jobnames and paths 11 | expect_equal(pull_jobnames(jobs), character(0)) 12 | expect_equal(pull_jobpaths(jobs), character(0)) 13 | expect_equal(pull_jobinfo(jobs), tibble::tibble()) 14 | 15 | jobs <- list() 16 | jobs[["toxic"]] <- workbch:::new_job( 17 | jobname = "toxic", 18 | description = "a song", 19 | owner = "britney", 20 | sentinel = FALSE) 21 | 22 | # check job names and paths: named character vectors 23 | expect_equal(pull_jobnames(jobs), c(toxic = "toxic")) 24 | expect_equal(pull_jobpaths(jobs), c(toxic = NA_character_)) 25 | expect_named(pull_jobinfo(jobs), c("jobname", "path", "idstring")) 26 | expect_equal(nrow(pull_jobinfo(jobs)), 0) 27 | 28 | # add a second job that does have a path 29 | jobs[["hitmebaby"]] <- new_job( 30 | jobname = "hitmebaby", 31 | description = "another song", 32 | owner = "britney", 33 | sentinel = FALSE, 34 | path = loc 35 | ) 36 | 37 | # check job names and paths: named character vectors 38 | expect_equal(pull_jobnames(jobs), c(toxic = "toxic", hitmebaby = "hitmebaby")) 39 | expect_equal(pull_jobpaths(jobs), c(toxic = NA_character_, hitmebaby = loc)) 40 | expect_named(pull_jobinfo(jobs), c("jobname", "path", "idstring")) 41 | expect_equal(nrow(pull_jobinfo(jobs)), 1) 42 | expect_equal(pull_jobinfo(jobs)$jobname, "hitmebaby") 43 | expect_equal(pull_jobinfo(jobs)$path, loc) 44 | expect_equal(pull_jobinfo(jobs)$idstring, jobs[["hitmebaby"]]$idstring) 45 | 46 | 47 | }) 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/testthat/test-readwrite.R: -------------------------------------------------------------------------------- 1 | 2 | # reset home to a tempory directory 3 | loc <- tempdir() 4 | options(workbch.home = loc) 5 | 6 | test_that("job_file returns the correct path", { 7 | expect_equal(job_file(), file.path(loc, "workbch_jobs.json")) 8 | }) 9 | 10 | test_that("reading and writing jobs works", { 11 | 12 | # delete the job file if for some reason it exists 13 | if(file.exists(job_file())) { 14 | file.remove(job_file()) 15 | } 16 | 17 | # check that job_read returns NULL for an empty file 18 | jobs <- job_read() 19 | expect_null(jobs) 20 | 21 | # create a list of jobs directly from the constructor 22 | jobs <- suppressWarnings(list(toxic = new_job( 23 | jobname = "toxic", description = "a song", owner = "britney" 24 | ))) 25 | job_write(jobs) 26 | 27 | # check job read has the correct info 28 | expect_equal(job_read(), jobs) 29 | 30 | # add a second job that does have a path 31 | jobs[["hitmebaby"]] <- suppressWarnings(new_job( 32 | jobname = "hitmebaby", 33 | description = "another song", 34 | owner = "britney", 35 | path = loc 36 | )) 37 | job_write(jobs) 38 | 39 | # check job read has the correct info 40 | expect_equal(job_read(), jobs) 41 | 42 | }) 43 | 44 | -------------------------------------------------------------------------------- /tests/testthat/test-sentinel.R: -------------------------------------------------------------------------------- 1 | 2 | loc <- tempdir() 3 | options(workbch.home = loc) 4 | 5 | test_that("sentinel_write works", { 6 | 7 | s <- file.path(loc, ".workbch") 8 | if(file.exists(s)) { file.remove(s) } 9 | 10 | sentinel_write(dir = loc, jobname = "toxic", idstring = "abcdeABCDE") 11 | 12 | expect_true(file.exists(s)) 13 | expect_equal(readLines(s), c("toxic", "abcdeABCDE")) 14 | 15 | }) 16 | 17 | 18 | test_that("sentinel_missing works", { 19 | 20 | # create path 21 | jobdir <- file.path(loc, "hitmebaby") 22 | if(!dir.exists(jobdir)) dir.create(jobdir) 23 | 24 | # write the job with no sentinel file 25 | jobs <- list() 26 | jobs[["hitmebaby"]] <- new_job( 27 | jobname = "hitmebaby", 28 | description = "a song", 29 | owner = "britney", 30 | path = jobdir, 31 | sentinel = FALSE 32 | ) 33 | job_write(jobs) 34 | 35 | # expect it to detect the missing sentinel 36 | expect_equal(sentinel_missing(), "hitmebaby") 37 | 38 | # now write the sentinel 39 | sentinel_write(dir = jobdir, jobname = "hitmebaby", 40 | idstring = jobs[["hitmebaby"]]$idstring) 41 | 42 | # expect nothing missing 43 | expect_length(sentinel_missing(), 0) 44 | 45 | }) 46 | -------------------------------------------------------------------------------- /tests/testthat/test-verify.R: -------------------------------------------------------------------------------- 1 | test_that("verify_onestring works", { 2 | msg <- "must be character and length 1" 3 | 4 | # should fail 5 | expect_error(verify_onestring(1), msg) 6 | expect_error(verify_onestring(character(0L)), msg) 7 | expect_error(verify_onestring(NA), msg) 8 | expect_error(verify_onestring(NULL), msg) 9 | expect_error(verify_onestring(c("a", "b")), msg) 10 | expect_error(verify_onestring(list("a")), msg) 11 | expect_error(verify_onestring(matrix("a")), msg) 12 | expect_error(verify_onestring(matrix(character(0L))), msg) 13 | expect_error(verify_onestring(TRUE), msg) 14 | 15 | # should succeed 16 | expect_true(verify_onestring("asdf ^asf")) 17 | }) 18 | 19 | test_that("verify_character works", { 20 | msg <- "must be character" 21 | 22 | # should fail 23 | expect_error(verify_character(1), msg) 24 | expect_error(verify_character(NA), msg) 25 | expect_error(verify_character(NULL), msg) 26 | expect_error(verify_character(list("a")), msg) 27 | expect_error(verify_character(matrix("a")), msg) 28 | expect_error(verify_character(matrix(character(0L))), msg) 29 | expect_error(verify_character(matrix(c("a","b"))), msg) 30 | expect_error(verify_character(TRUE), msg) 31 | 32 | # should succeed 33 | expect_true(verify_character(character(0L))) 34 | expect_true(verify_character(NA_character_)) 35 | expect_true(verify_character("aasdfasdf")) 36 | expect_true(verify_character(c("asdfasdf", "a"))) 37 | }) 38 | 39 | 40 | test_that("verify_status works", { 41 | 42 | # cursory check that verify_onestring is called 43 | msg <- "must be character and length 1" 44 | expect_error(verify_status(1), msg) 45 | expect_error(verify_status(c("active", "inactive")), msg) 46 | 47 | # check that invalid status string are not accepted 48 | msg <- "must be 'active', 'inactive'" 49 | expect_error(verify_status("blarhg"), msg) 50 | expect_error(verify_status("nactive"), msg) 51 | expect_error(verify_status("Active"), msg) 52 | expect_error(verify_status(" active"), msg) 53 | expect_error(verify_status("active "), msg) 54 | expect_error(verify_status("ac tive"), msg) 55 | 56 | # should succeed 57 | expect_true(verify_status("active")) 58 | expect_true(verify_status("inactive")) 59 | expect_true(verify_status("complete")) 60 | expect_true(verify_status("abandoned")) 61 | expect_true(verify_status("masked")) 62 | 63 | }) 64 | 65 | 66 | test_that("verify_priority works", { 67 | 68 | # check that invalid priorities throw errors 69 | msg <- "job priority must be a positive integer" 70 | expect_error(verify_priority(0), msg) 71 | expect_error(verify_priority(-1), msg) 72 | expect_error(verify_priority(NaN), msg) 73 | expect_error(verify_priority(1.2), msg) 74 | expect_error(verify_priority("a"), msg) 75 | expect_error(verify_priority(1:2), msg) 76 | expect_error(verify_priority(NULL), msg) 77 | expect_error(verify_priority(Inf), msg) 78 | expect_error(verify_priority(NA_integer_), msg) 79 | expect_error(verify_priority(NA), msg) 80 | 81 | # should succeed 82 | expect_true(verify_priority(1)) 83 | expect_true(verify_priority(2)) 84 | expect_true(verify_priority(3)) 85 | 86 | }) 87 | 88 | test_that("verify_description works", { 89 | 90 | # cursory check that verify_onestring is called 91 | msg <- "must be character and length 1" 92 | expect_error(verify_description(1), msg) 93 | expect_error(verify_description(c("blah blah", "blah")), msg) 94 | 95 | # expect success 96 | expect_true(verify_description("a job to do something")) 97 | 98 | }) 99 | 100 | test_that("verify_site works", { 101 | 102 | # cursory check that verify_onestring is called 103 | msg <- "must be character and length 1" 104 | expect_error(verify_site(1), msg) 105 | expect_error(verify_site(c("blah blah", "blah")), msg) 106 | 107 | # expect success 108 | expect_true(verify_site("gitblah")) 109 | 110 | }) 111 | 112 | test_that("verify_link works", { 113 | 114 | # cursory check that verify_onestring is called 115 | msg <- "must be character and length 1" 116 | expect_error(verify_link(1), msg) 117 | expect_error(verify_link(c("blah blah", "blah")), msg) 118 | 119 | # expect success 120 | expect_true(verify_link("https://gitblub.com/fuser/depot")) 121 | 122 | }) 123 | 124 | test_that("verify_owner works", { 125 | 126 | # cursory check that verify_onestring is called 127 | msg <- "must be character and length 1" 128 | expect_error(verify_owner(1), msg) 129 | expect_error(verify_owner(c("blah blah", "blah")), msg) 130 | 131 | # expect success 132 | expect_true(verify_owner("britney")) 133 | 134 | }) 135 | 136 | 137 | test_that("verify_jobname works", { 138 | 139 | # cursory check that verify_onestring is called 140 | msg <- "must be character and length 1" 141 | expect_error(verify_jobname(1), msg) 142 | expect_error(verify_jobname(c("blah blah", "blah")), msg) 143 | 144 | # expect success 145 | expect_true(verify_jobname("toxic")) 146 | 147 | }) 148 | 149 | 150 | test_that("verify_path works", { 151 | 152 | # cursory check that verify_onestring is called 153 | msg <- "must be character and length 1" 154 | expect_error(verify_path(1), msg) 155 | expect_error(verify_path(c("blah blah", "blah")), msg) 156 | 157 | # expect success 158 | expect_true(verify_path("~/GitHub/")) 159 | 160 | }) 161 | 162 | 163 | test_that("verify_jobexists / verify_jobmissing work", { 164 | 165 | loc <- tempdir() 166 | options(workbch.home = loc) 167 | 168 | # delete the job file if for some reason it exists 169 | if(file.exists(job_file())) { 170 | file.remove(job_file()) 171 | } 172 | jobs <- job_read() 173 | 174 | # verification tests (here because that was the original file structure) 175 | expect_true(verify_jobmissing("toxic", jobs, strict = FALSE)) 176 | expect_true(verify_jobmissing("hitmebaby", jobs, strict = FALSE)) 177 | 178 | # create a list of jobs directly from the constructor 179 | jobs <- suppressWarnings(list(toxic = new_job( 180 | jobname = "toxic", description = "a song", owner = "britney" 181 | ))) 182 | job_write(jobs) 183 | 184 | # verification tests (here because that was the original file structure) 185 | expect_true(verify_jobexists("toxic", jobs, strict = FALSE)) 186 | expect_true(verify_jobmissing("hitmebaby", jobs, strict = FALSE)) 187 | 188 | # add a second job that does have a path 189 | jobs[["hitmebaby"]] <- suppressWarnings(new_job( 190 | jobname = "hitmebaby", 191 | description = "another song", 192 | owner = "britney", 193 | path = loc 194 | )) 195 | job_write(jobs) 196 | 197 | # verification tests (here because that was the original file structure) 198 | expect_true(verify_jobexists("toxic", jobs, strict = FALSE)) 199 | expect_true(verify_jobexists("hitmebaby", jobs, strict = FALSE)) 200 | 201 | }) 202 | 203 | 204 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/setup.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setting up your work bench" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | knitr::opts_chunk$set( 7 | collapse = TRUE, 8 | comment = "#>" 9 | ) 10 | ``` 11 | 12 | 13 | ```{r, include = FALSE} 14 | knitr::opts_chunk$set( 15 | collapse = TRUE, 16 | comment = "#>" 17 | ) 18 | ``` 19 | 20 | The workbch ("work bench") package provides simple utility functions to help the R user keep track of projects and navigate between them. The package is designed around the concept of *jobs*, where a job might correspond to an RStudio project, a git repository, a research project or indeed all of the above. Jobs are assumed to be stored in a single folder, but can be associated with URLs (e.g., on GitHub, Overleaf, OSF, or elsewhere). The package is intended to be used interactively, though most functions can be called from scripts if needed. 21 | 22 | The package consists of nine functions. Three functions are used to create, modify, and delete jobs 23 | 24 | - `job_create()`. Create a new job 25 | - `job_modify()`. Modify or delete an existing job 26 | - `job_seek()`. Scans a directory recursively to find jobs 27 | 28 | There are three functions that are useful for navigation: 29 | 30 | - `job_open()`. Opens an RStudio project or changes working directory 31 | - `job_openurl()`. Opens a URL associated with a job in a browser window 32 | - `job_home()`. Returns the path to the job folder 33 | 34 | There are three functions that are useful for keeping track of projects: 35 | 36 | - `job_gitreport()`. Shows the git status of all jobs 37 | - `job_glimpse()`. Shows all information stored about a job 38 | - `job_list()`. Displays a table summarising all jobs, or a subset of them. 39 | 40 | 41 | 42 | ## Set up 43 | 44 | The easiest way to get started with workbch is to use `job_seek()` to scan for jobs to track. However, before you do this, the workbch package needs to know two things: the "home" folder where it should save its own files, and the top level "search" folder (or folders) where it should look for jobs. The simplest way to do this is to add a few lines to your .Rprofile file, using `usethis::edit_r_profile()` or simply using a text editor. To keep this vignette simple, I'll set it up so that the workbch package only scans my "fun" GitHub folder: 45 | 46 | ```{r} 47 | # workbch options to add to .Rprofile 48 | options(workbch.home = "/home/danielle/GitHub/fun") 49 | options(workbch.search = "/home/danielle/GitHub/fun") 50 | suppressMessages(require(workbch)) 51 | ``` 52 | 53 | Once you have saved this to the .Rprofile and restarted the R session, you're ready to start adding jobs. 54 | 55 | ## Adding jobs 56 | 57 | The simplest way to add jobs is to call `job_seek()`. To make things a little easier, when I do this I'll set a default value for the `owner` of created jobs: 58 | 59 | ```{r, eval=FALSE} 60 | job_seek(owner = "Danielle") 61 | ``` 62 | 63 | When I do this, I am shown a message indicating that `job_seek()` has discovered five folders that might correspond to jobs. After agreeing to continue, it shows me what it has found, one job at a time. Here is the first one: 64 | 65 | ``` 66 | Suggested values: 67 | 68 | Path: /home/danielle/GitHub/fun/rainbowr 69 | Job name: rainbowr 70 | Description: rainbowr 71 | Owner: Danielle 72 | Status: active 73 | Priority: 1 74 | Tags: 75 | Git remote (site): github 76 | Git remote (url): https://github.com/djnavarro/rainbowr/ 77 | 78 | Track this folder with workbch? [y/n] 79 | ``` 80 | 81 | As this is a job I wish to track, I type `y`, and then R will prompt me either to accept each of these suggested values (by pressing enter) or to type in the value I would like worbch to store: 82 | 83 | ``` 84 | Type new values or press enter to use the suggested value: 85 | 86 | Job name...... 87 | Description... generates LGBT hex stickers 88 | Owner......... 89 | Status........ inactive 90 | Priority...... 91 | Tags.......... 92 | ``` 93 | 94 | In this example I have retained the suggested value for `jobname` ("rainbowr"), `owner` ("Danielle"), `priority` (1) and `tags` (no tags), but chosen to overwrite the `description` (almost always a good idea) and the `status`. This process repeats a few times, and the end result is a JSON file (`workbch_jobs.json`) in the "home" folder. Note also that this will write a file called `.workbch` inside the folder for every tracked job. This file contains almost no information: it lists the job name along with a random identification string, and is only used to make it possible for the workbch package to recover the locations of projects if they are moved. 95 | 96 | At this point I'm ready to go! 97 | 98 | ## Listing jobs 99 | 100 | ```{r eval=FALSE} 101 | job_list() 102 | ``` 103 | 104 | ``` 105 | # A tibble: 4 x 5 106 | jobname owner priority status description 107 | 108 | 1 asciify Danielle 1 active make ASCII art from images 109 | 2 rainbowr Danielle 1 inactive generates LGBT hex stickers 110 | 3 brownianbridge Danielle 2 inactive generate Brownian bridge animations 111 | 4 skyliner Danielle 2 inactive create gifs of the Sydney skyline 112 | ``` 113 | 114 | By default, the `job_list()` function will only show jobs that have `priority` 1 or 2, and whose `status` is listed as `active` or `inactive` (i.e., `abandoned`, `masked` and `complete` jobs are not displayed). However it is easy to customise the output by entering a search query. At present, the behaviour of this search is somewhat limited: it will return every job for which at least one of the fields is an exact match to the query string. So for example: 115 | 116 | ```{r, eval=FALSE} 117 | job_list("inactive") 118 | ``` 119 | 120 | would return all the jobs with an "inactive" status: 121 | 122 | ``` 123 | # A tibble: 3 x 5 124 | jobname owner priority status description 125 | 126 | 1 rainbowr Danielle 1 inactive generates LGBT hex stickers 127 | 2 brownianbridge Danielle 2 inactive generate Brownian bridge animations 128 | 3 skyliner Danielle 2 inactive create gifs of the Sydney skyline 129 | ``` 130 | 131 | whereas: 132 | 133 | ```{r, eval=FALSE} 134 | job_list(1) 135 | ``` 136 | 137 | returns jobs that have priority 1: 138 | 139 | ``` 140 | # A tibble: 2 x 5 141 | jobname owner priority status description 142 | 143 | 1 asciify Danielle 1 active make ASCII art from images 144 | 2 rainbowr Danielle 1 inactive generates LGBT hex stickers 145 | ``` 146 | 147 | This can also be used to select those jobs that match a tag, or are owned by a particular person, and so on. 148 | 149 | ## Screencast 150 | 151 | 155 | -------------------------------------------------------------------------------- /vignettes/setup.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djnavarro/workbch/6cc7a28e92a24acee1f61ad60c124f701108a96a/vignettes/setup.mp4 -------------------------------------------------------------------------------- /vignettes/usage.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Using the workbch package" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | knitr::opts_chunk$set( 7 | collapse = TRUE, 8 | comment = "#>" 9 | ) 10 | ``` 11 | 12 | ```{r setup} 13 | library(workbch) 14 | ``` 15 | 16 | ## Navigation 17 | 18 | One function of the workbch package is to make navigation a little easier: `job_open()` allows you to switch between jobs, and `job_openurl()` opens a URL linked to the job in a browser window. The screencast below illustrates this: 19 | 20 | 24 | 25 | ## Git report 26 | 27 | A second function of workbch is to keep track of the git status of various projects. A common workflow problem (for me, at least) is that I sometimes forget to commit changes or push them upstream, and then forget which projects I've left in this "unclean" state. The `job_gitreport()` function aims to make this easier by checking the git status for all jobs, and reporting those jobs that are not in a clean state. For example: 28 | 29 | ```{r, eval=FALSE} 30 | job_gitreport() 31 | ``` 32 | ``` 33 | # A tibble: 3 x 6 34 | jobname staged unstaged untracked ahead behind 35 | 36 | 1 conjunction 0 1 1 3 0 37 | 2 psychtheory 0 0 1 0 0 38 | 3 tidylsrbook 0 24 12 0 0 39 | ``` 40 | 41 | Screencast illustrating how `job_gitreport()` works: 42 | 43 | 47 | 48 | 49 | ## Moving jobs 50 | 51 | A third function of the workbch package is keeping track of job locations when they move around on the local machine, as long as they stay somewhere on the workbch search path (i.e., the "workbch.search" option). It does this by matching information stored in the ".workbch" sentinel file to entries in the "workbch_jobs.json" file. There are two functions that are useful for this: 52 | 53 | - `job_list()` throws a warning if a job folder has moved 54 | - `job_seek()` will ask if you want to update the location 55 | 56 | This is illustrated in the screencast below: 57 | 58 | 62 | 63 | 64 | ## Modifying jobs 65 | 66 | A common task is to update the information associated with a job. The priority may change, it may be assigned a new owner or acquire a new URL, etc. The `job_modify()` function can be used for this, as illustrated below: 67 | 68 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /workbch.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | --------------------------------------------------------------------------------