1## git2r, R bindings to the libgit2 library.
2## Copyright (C) 2013-2019 The git2r contributors
3##
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License, version 2,
6## as published by the Free Software Foundation.
7##
8## git2r is distributed in the hope that it will be useful,
9## but WITHOUT ANY WARRANTY; without even the implied warranty of
10## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11## GNU General Public License for more details.
12##
13## You should have received a copy of the GNU General Public License along
14## with this program; if not, write to the Free Software Foundation, Inc.,
15## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
17##' Coerce Git repository to a \code{data.frame}
18##'
19##' The commits in the repository are coerced to a \code{data.frame}
20##'
21##'
22##' The \code{data.frame} have the following columns:
23##' \describe{
24##'
25##'   \item{sha}{
26##'     The 40 character hexadecimal string of the SHA-1
27##'   }
28##'
29##'   \item{summary}{
30##'     the short "summary" of the git commit message.
31##'   }
32##'
33##'   \item{message}{
34##'     the full message of a commit
35##'   }
36##'
37##'   \item{author}{
38##'     full name of the author
39##'   }
40##'
41##'   \item{email}{
42##'     email of the author
43##'   }
44##'
45##'   \item{when}{
46##'     time when the commit happened
47##'   }
48##'
49##' }
50##' @param x The repository \code{object}
51##' @param ... Additional arguments. Not used.
52##' @return \code{data.frame}
53##' @export
54##' @examples
55##' \dontrun{
56##' ## Initialize a temporary repository
57##' path <- tempfile(pattern="git2r-")
58##' dir.create(path)
59##' repo <- init(path)
60##'
61##' ## Create a user
62##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
63##'
64##' ## Create three files and commit
65##' writeLines("First file",  file.path(path, "example-1.txt"))
66##' writeLines("Second file", file.path(path, "example-2.txt"))
67##' writeLines("Third file",  file.path(path, "example-3.txt"))
68##' add(repo, "example-1.txt")
69##' commit(repo, "Commit first file")
70##' add(repo, "example-2.txt")
71##' commit(repo, "Commit second file")
72##' add(repo, "example-3.txt")
73##' commit(repo, "Commit third file")
74##'
75##' ## Coerce commits to a data.frame
76##' df <- as.data.frame(repo)
77##' df
78##' }
79as.data.frame.git_repository <- function(x, ...) {
80    do.call("rbind", lapply(commits(x), as.data.frame))
81}
82
83##' Open a repository
84##'
85##' @param path A path to an existing local git repository.
86##' @param discover Discover repository from path. Default is TRUE.
87##' @return A \code{git_repository} object with entries:
88##' \describe{
89##'   \item{path}{
90##'     Path to a git repository
91##'   }
92##' }
93##' @export
94##' @examples
95##' \dontrun{
96##' ## Initialize a temporary repository
97##' path <- tempfile(pattern="git2r-")
98##' dir.create(path)
99##' repo <- init(path)
100##'
101##' # Configure a user
102##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
103##'
104##' ## Create a file, add and commit
105##' writeLines("Hello world!", file.path(path, "test-1.txt"))
106##' add(repo, 'test-1.txt')
107##' commit_1 <- commit(repo, "Commit message")
108##'
109##' ## Make one more commit
110##' writeLines(c("Hello world!", "HELLO WORLD!"),
111##'            file.path(path, "test-1.txt"))
112##' add(repo, 'test-1.txt')
113##' commit(repo, "Next commit message")
114##'
115##' ## Create one more file
116##' writeLines("Hello world!",
117##'            file.path(path, "test-2.txt"))
118##'
119##' ## Brief summary of repository
120##' repo
121##'
122##' ## Summary of repository
123##' summary(repo)
124##'
125##' ## Workdir of repository
126##' workdir(repo)
127##'
128##' ## Check if repository is bare
129##' is_bare(repo)
130##'
131##' ## Check if repository is empty
132##' is_empty(repo)
133##'
134##' ## Check if repository is a shallow clone
135##' is_shallow(repo)
136##'
137##' ## List all references in repository
138##' references(repo)
139##'
140##' ## List all branches in repository
141##' branches(repo)
142##'
143##' ## Get HEAD of repository
144##' repository_head(repo)
145##'
146##' ## Check if HEAD is head
147##' is_head(repository_head(repo))
148##'
149##' ## Check if HEAD is local
150##' is_local(repository_head(repo))
151##'
152##' ## List all tags in repository
153##' tags(repo)
154##' }
155repository <- function(path = ".", discover = TRUE) {
156    if (isTRUE(discover)) {
157        path <- discover_repository(path)
158        if (is.null(path))
159            stop("The 'path' is not in a git repository")
160    } else {
161        path <- normalizePath(path, winslash = "/", mustWork = TRUE)
162        if (!file.info(path)$isdir)
163            stop("'path' is not a directory")
164    }
165
166    if (!isTRUE(.Call(git2r_repository_can_open, path)))
167        stop("Unable to open repository at 'path'")
168
169    structure(list(path = path), class = "git_repository")
170}
171
172##' Init a repository
173##'
174##' @param path A path to where to init a git repository
175##' @param bare If TRUE, a Git repository without a working directory
176##'     is created at the pointed path. If FALSE, provided path will
177##'     be considered as the working directory into which the .git
178##'     directory will be created.
179##' @return A \code{git_repository} object
180##' @export
181##' @seealso \link{repository}
182##' @examples
183##' \dontrun{
184##' ## Initialize a repository
185##' path <- tempfile(pattern="git2r-")
186##' dir.create(path)
187##' repo <- init(path)
188##' is_bare(repo)
189##'
190##' ## Initialize a bare repository
191##' path_bare <- tempfile(pattern="git2r-")
192##' dir.create(path_bare)
193##' repo_bare <- init(path_bare, bare = TRUE)
194##' is_bare(repo_bare)
195##' }
196init <- function(path = ".", bare = FALSE) {
197    path <- normalizePath(path, winslash = "/", mustWork = TRUE)
198    if (!file.info(path)$isdir)
199        stop("'path' is not a directory")
200    .Call(git2r_repository_init, path, bare)
201    repository(path)
202}
203
204##' Clone a remote repository
205##'
206##' @param url The remote repository to clone
207##' @param local_path Local directory to clone to.
208##' @param bare Create a bare repository. Default is FALSE.
209##' @param branch The name of the branch to checkout. Default is NULL
210##'     which means to use the remote's default branch.
211##' @param checkout Checkout HEAD after the clone is complete. Default
212##'     is TRUE.
213##' @param credentials The credentials for remote repository
214##'     access. Default is NULL. To use and query an ssh-agent for the
215##'     ssh key credentials, let this parameter be NULL (the default).
216##' @param progress Show progress. Default is TRUE.
217##' @return A \code{git_repository} object.
218##' @seealso \link{repository}, \code{\link{cred_user_pass}},
219##'     \code{\link{cred_ssh_key}}
220##' @export
221##' @examples
222##' \dontrun{
223##' ## Initialize repository
224##' path_repo_1 <- tempfile(pattern="git2r-")
225##' path_repo_2 <- tempfile(pattern="git2r-")
226##' dir.create(path_repo_1)
227##' dir.create(path_repo_2)
228##' repo_1 <- init(path_repo_1)
229##'
230##' ## Config user and commit a file
231##' config(repo_1, user.name = "Alice", user.email = "alice@@example.org")
232##'
233##' ## Write to a file and commit
234##' writeLines(
235##'     "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do",
236##'     file.path(path_repo_1, "example.txt"))
237##' add(repo_1, "example.txt")
238##' commit(repo_1, "First commit message")
239##'
240##' ## Change file and commit
241##' lines <- c(
242##'   "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do",
243##'   "eiusmod tempor incididunt ut labore et dolore magna aliqua.")
244##' writeLines(lines, file.path(path_repo_1, "example.txt"))
245##' add(repo_1, "example.txt")
246##' commit(repo_1, "Second commit message")
247##'
248##' ## Change file again and commit.
249##' lines <- c(
250##'   "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do",
251##'   "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad",
252##'   "minim veniam, quis nostrud exercitation ullamco laboris nisi ut")
253##' writeLines(lines, file.path(path_repo_1, "example.txt"))
254##' add(repo_1, "example.txt")
255##' commit(repo_1, "Third commit message")
256##'
257##' ## Clone to second repository
258##' repo_2 <- clone(path_repo_1, path_repo_2)
259##'
260##' ## List commits in repositories
261##' commits(repo_1)
262##' commits(repo_2)
263##' }
264clone <- function(url         = NULL,
265                  local_path  = NULL,
266                  bare        = FALSE,
267                  branch      = NULL,
268                  checkout    = TRUE,
269                  credentials = NULL,
270                  progress    = TRUE) {
271    .Call(git2r_clone, url, local_path, bare,
272          branch, checkout, credentials, progress)
273    repository(local_path)
274}
275
276##' Get HEAD for a repository
277##'
278##' @param x The repository \code{x} to check head
279##' @param ... Additional arguments. Unused.
280##' @return NULL if unborn branch or not found. A git_branch if not a
281##'     detached head. A git_commit if detached head
282##' @importFrom utils head
283##' @export
284##' @examples
285##' \dontrun{
286##' ## Create and initialize a repository in a temporary directory
287##' path <- tempfile(pattern="git2r-")
288##' dir.create(path)
289##' repo <- init(path)
290##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
291##'
292##' ## Create a file, add and commit
293##' writeLines("Hello world!", file.path(path, "example.txt"))
294##' add(repo, "example.txt")
295##' commit(repo, "Commit message")
296##'
297##' ## Get HEAD of repository
298##' repository_head(repo)
299##' }
300head.git_repository <- function(x, ...) {
301    .Deprecated("repository_head")
302    .Call(git2r_repository_head, x)
303}
304
305##' @export
306utils::head
307
308##' Get HEAD for a repository
309##'
310##' @template repo-param
311##' @return NULL if unborn branch or not found. A git_branch if not a
312##'     detached head. A git_commit if detached head
313##' @export
314##' @examples
315##' \dontrun{
316##' ## Create and initialize a repository in a temporary directory
317##' path <- tempfile(pattern="git2r-")
318##' dir.create(path)
319##' repo <- init(path)
320##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
321##'
322##' ## Create a file, add and commit
323##' writeLines("Hello world!", file.path(path, "example.txt"))
324##' add(repo, "example.txt")
325##' commit(repo, "Commit message")
326##'
327##' ## Get HEAD of repository
328##' repository_head(repo)
329##' }
330repository_head <- function(repo = ".") {
331    .Call(git2r_repository_head, lookup_repository(repo))
332}
333
334##' Check if repository is bare
335##'
336##' @template repo-param
337##' @return \code{TRUE} if bare repository, else \code{FALSE}
338##' @seealso \link{init}
339##' @export
340##' @examples
341##' \dontrun{
342##' ## Initialize a repository
343##' path <- tempfile(pattern="git2r-")
344##' dir.create(path)
345##' repo <- init(path)
346##' is_bare(repo)
347##'
348##' ## Initialize a bare repository
349##' path_bare <- tempfile(pattern="git2r-")
350##' dir.create(path_bare)
351##' repo_bare <- init(path_bare, bare = TRUE)
352##' is_bare(repo_bare)
353##' }
354is_bare <- function(repo = ".") {
355    .Call(git2r_repository_is_bare, lookup_repository(repo))
356}
357
358##' Check if HEAD of repository is detached
359##'
360##' @template repo-param
361##' @return \code{TRUE} if repository HEAD is detached, else
362##'     \code{FALSE}.
363##' @export
364##' @examples
365##' \dontrun{
366##' ## Create and initialize a repository in a temporary directory
367##' path <- tempfile(pattern="git2r-")
368##' dir.create(path)
369##' repo <- init(path)
370##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
371##'
372##' ## Create a file, add and commit
373##' lines <- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do"
374##' writeLines(lines, file.path(path, "example.txt"))
375##' add(repo, "example.txt")
376##' commit_1 <- commit(repo, "Commit message 1")
377##'
378##' ## Change file, add and commit
379##' lines <- c(
380##'   "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do",
381##'   "eiusmod tempor incididunt ut labore et dolore magna aliqua.")
382##' writeLines(lines, file.path(path, "example.txt"))
383##' add(repo, "example.txt")
384##' commit(repo, "Commit message 2")
385##'
386##' ## HEAD of repository is not detached
387##' is_detached(repo)
388##'
389##' ## Checkout first commit
390##' checkout(commit_1)
391##'
392##' ## HEAD of repository is detached
393##' is_detached(repo)
394##' }
395is_detached <- function(repo = ".") {
396    .Call(git2r_repository_head_detached, lookup_repository(repo))
397}
398
399##' Check if repository is empty
400##'
401##' @template repo-param
402##' @return \code{TRUE} if repository is empty else \code{FALSE}.
403##' @export
404##' @examples
405##' \dontrun{
406##' ## Initialize a temporary repository
407##' path <- tempfile(pattern="git2r-")
408##' dir.create(path)
409##' repo <- init(path)
410##'
411##' ## Create a user
412##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
413##'
414##' ## Check if it's an empty repository
415##' is_empty(repo)
416##'
417##' ## Commit a file
418##' writeLines("Hello world!", file.path(path, "example.txt"))
419##' add(repo, "example.txt")
420##' commit(repo, "First commit message")
421##'
422##' ## Check if it's an empty repository
423##' is_empty(repo)
424##' }
425is_empty <- function(repo = ".") {
426    .Call(git2r_repository_is_empty, lookup_repository(repo))
427}
428
429##' Determine if a directory is in a git repository
430##'
431##' The lookup start from path and walk across parent directories if
432##' nothing has been found.
433##' @param path The path to the directory.
434##' @return TRUE if directory is in a git repository else FALSE
435##' @export
436##' @examples
437##' \dontrun{
438##' ## Initialize a temporary repository
439##' path <- tempfile(pattern="git2r-")
440##' dir.create(path)
441##' repo <- init(path)
442##'
443##' ## Create a user
444##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
445##'
446##' ## Check if path is in a git repository
447##' in_repository(path)
448##'
449##' ## Check if working directory is in a git repository
450##' setwd(path)
451##' in_repository()
452##' }
453in_repository <- function(path = ".") {
454    !is.null(discover_repository(path))
455}
456
457##' Determine if the repository is a shallow clone
458##'
459##' @template repo-param
460##' @return \code{TRUE} if shallow clone, else \code{FALSE}
461##' @export
462##' @examples
463##' \dontrun{
464##' ## Initialize repository
465##' path_repo_1 <- tempfile(pattern="git2r-")
466##' path_repo_2 <- tempfile(pattern="git2r-")
467##' dir.create(path_repo_1)
468##' dir.create(path_repo_2)
469##' repo_1 <- init(path_repo_1)
470##'
471##' ## Config user and commit a file
472##' config(repo_1, user.name = "Alice", user.email = "alice@@example.org")
473##'
474##' ## Write to a file and commit
475##' lines <- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do"
476##' writeLines(lines, file.path(path_repo_1, "example.txt"))
477##' add(repo_1, "example.txt")
478##' commit(repo_1, "First commit message")
479##'
480##' ## Change file and commit
481##' lines <- c(
482##'   "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do",
483##'   "eiusmod tempor incididunt ut labore et dolore magna aliqua.")
484##' writeLines(lines, file.path(path_repo_1, "example.txt"))
485##' add(repo_1, "example.txt")
486##' commit(repo_1, "Second commit message")
487##'
488##' ## Change file again and commit.
489##' lines <- c(
490##'   "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do",
491##'   "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad",
492##'   "minim veniam, quis nostrud exercitation ullamco laboris nisi ut")
493##' writeLines(lines, file.path(path_repo_1, "example.txt"))
494##' add(repo_1, "example.txt")
495##' commit(repo_1, "Third commit message")
496##'
497##' ## Clone to second repository
498##' repo_2 <- clone(path_repo_1, path_repo_2)
499##'
500##' ## Check if it's a shallow clone
501##' is_shallow(repo_2)
502##' }
503is_shallow <- function(repo = ".") {
504    .Call(git2r_repository_is_shallow, lookup_repository(repo))
505}
506
507##' Lookup
508##'
509##' Lookup one object in a repository.
510##' @template repo-param
511##' @param sha The identity of the object to lookup. Must be 4 to 40
512##' characters long.
513##' @return a \code{git_blob} or \code{git_commit} or \code{git_tag}
514##' or \code{git_tree} object
515##' @export
516##' @examples
517##' \dontrun{
518##' ## Initialize a temporary repository
519##' path <- tempfile(pattern="git2r-")
520##' dir.create(path)
521##' repo <- init(path)
522##'
523##' ## Create a user and commit a file
524##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
525##' lines <- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do"
526##' writeLines(lines, file.path(path, "example.txt"))
527##' add(repo, "example.txt")
528##' commit_1 <- commit(repo, "First commit message")
529##'
530##' ## Create tag
531##' tag(repo, "Tagname", "Tag message")
532##'
533##' ## First, get SHAs to lookup in the repository
534##' sha_commit <- sha(commit_1)
535##' sha_tree <- sha(tree(commit_1))
536##' sha_blob <- sha(tree(commit_1)["example.txt"])
537##' sha_tag <- sha(tags(repo)[[1]])
538##'
539##' ## SHAs
540##' sha_commit
541##' sha_tree
542##' sha_blob
543##' sha_tag
544##'
545##' ## Lookup objects
546##' lookup(repo, sha_commit)
547##' lookup(repo, sha_tree)
548##' lookup(repo, sha_blob)
549##' lookup(repo, sha_tag)
550##'
551##' ## Lookup objects, using only the first seven characters
552##' lookup(repo, substr(sha_commit, 1, 7))
553##' lookup(repo, substr(sha_tree, 1, 7))
554##' lookup(repo, substr(sha_blob, 1, 7))
555##' lookup(repo, substr(sha_tag, 1, 7))
556##' }
557lookup <- function(repo = ".", sha = NULL) {
558    .Call(git2r_object_lookup, lookup_repository(repo), sha)
559}
560
561##' Lookup the commit related to a git object
562##'
563##' Lookup the commit related to a git_reference, git_tag or
564##' git_branch object.
565##' @param object a git object to get the related commit from.
566##' @return A git commit object.
567##' @export
568##' @examples \dontrun{
569##' ## Create a directory in tempdir
570##' path <- tempfile(pattern="git2r-")
571##' dir.create(path)
572##'
573##' ## Initialize a repository
574##' repo <- init(path)
575##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
576##'
577##' ## Create a file, add and commit
578##' lines <- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do"
579##' writeLines(lines, con = file.path(path, "test.txt"))
580##' add(repo, "test.txt")
581##' commit(repo, "Commit message 1")
582##'
583##' ## Get the commit pointed to by the 'master' branch
584##' lookup_commit(repository_head(repo))
585##'
586##' ## Create a tag
587##' a_tag <- tag(repo, "Tagname", "Tag message")
588##'
589##' ## Get the commit pointed to by 'a_tag'
590##' lookup_commit(a_tag)
591##' }
592lookup_commit <- function(object) {
593    UseMethod("lookup_commit", object)
594}
595
596##' @rdname lookup_commit
597##' @export
598lookup_commit.git_branch <- function(object) {
599    lookup(object$repo, branch_target(object))
600}
601
602##' @rdname lookup_commit
603##' @export
604lookup_commit.git_commit <- function(object) {
605    object
606}
607
608##' @rdname lookup_commit
609##' @export
610lookup_commit.git_tag <- function(object) {
611    lookup(object$repo, object$target)
612}
613
614##' @rdname lookup_commit
615##' @export
616lookup_commit.git_reference <- function(object) {
617    lookup_commit(lookup(object$repo, object$sha))
618}
619
620##' @export
621print.git_repository <- function(x, ...) {
622    if (any(is_empty(x), is.null(repository_head(x)))) {
623        cat(sprintf("Local:    %s\n", workdir(x)))
624        cat("Head:     nothing commited (yet)\n")
625    } else {
626        if (is_detached(x)) {
627            cat(sprintf("Local:    (detached) %s\n", workdir(x)))
628
629            h <- repository_head(x)
630        } else {
631            cat(sprintf("Local:    %s %s\n",
632                        repository_head(x)$name,
633                        workdir(x)))
634
635            h <- repository_head(x)
636            u <- branch_get_upstream(h)
637            if (!is.null(u)) {
638                rn <- branch_remote_name(u)
639                cat(sprintf("Remote:   %s @ %s (%s)\n",
640                            substr(u$name, nchar(rn) + 2, nchar(u$name)),
641                            rn,
642                            branch_remote_url(u)))
643            }
644
645            h <- lookup(x, branch_target(repository_head(x)))
646        }
647
648        cat(sprintf("Head:     [%s] %s: %s\n",
649                    substring(h$sha, 1, 7),
650                    substring(as.character(h$author$when), 1, 10),
651                    h$summary))
652    }
653
654    invisible(x)
655}
656
657##' Summary of repository
658##'
659##' @param object The repository \code{object}
660##' @param ... Additional arguments affecting the summary produced.
661##' @return None (invisible 'NULL').
662##' @export
663##' @examples
664##' \dontrun{
665##' ## Initialize a repository
666##' path <- tempfile(pattern="git2r-")
667##' dir.create(path)
668##' repo <- init(path)
669##'
670##' ## Config user
671##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
672##'
673##' ## Create a file
674##' writeLines("Hello world!", file.path(path, "test.txt"))
675##' summary(repo)
676##'
677##' ## Add file
678##' add(repo, "test.txt")
679##' summary(repo)
680##'
681##' ## Commit
682##' commit(repo, "First commit message")
683##' summary(repo)
684##'
685##' ## Change the file
686##' writeLines(c("Hello again!", "Here is a second line", "And a third"),
687##'            file.path(path, "test.txt"))
688##' summary(repo)
689##'
690##' ## Add file and commit
691##' add(repo, "test.txt")
692##' commit(repo, "Second commit message")
693##' summary(repo)
694##'}
695summary.git_repository <- function(object, ...) {
696    print(object)
697    cat("\n")
698
699    n_branches <- sum(!is.na(unique(sapply(branches(object),
700                                           branch_target))))
701    n_tags <- sum(!is.na(unique(vapply(tags(object), "[[",
702                                       character(1), "sha"))))
703
704    work <- commits(object)
705    n_commits <- length(work)
706    n_authors <- length(unique(vapply(lapply(work, "[[", "author"),
707                                      "[[", character(1), "name")))
708
709    s <- .Call(git2r_status_list, object, TRUE, TRUE, TRUE, FALSE, TRUE)
710    n_ignored <- length(s$ignored)
711    n_untracked <- length(s$untracked)
712    n_unstaged <- length(s$unstaged)
713    n_staged <- length(s$staged)
714
715    n_stashes <- length(stash_list(object))
716
717    ## Determine max characters needed to display numbers
718    n <- max(vapply(c(n_branches, n_tags, n_commits, n_authors,
719                      n_stashes, n_ignored, n_untracked,
720                      n_unstaged, n_staged),
721                    nchar,
722                    numeric(1)))
723
724    fmt <- paste0("Branches:        %", n, "i\n",
725                  "Tags:            %", n, "i\n",
726                  "Commits:         %", n, "i\n",
727                  "Contributors:    %", n, "i\n",
728                  "Stashes:         %", n, "i\n",
729                  "Ignored files:   %", n, "i\n",
730                  "Untracked files: %", n, "i\n",
731                  "Unstaged files:  %", n, "i\n",
732                  "Staged files:    %", n, "i\n")
733    cat(sprintf(fmt, n_branches, n_tags, n_commits, n_authors,
734                n_stashes, n_ignored, n_untracked, n_unstaged,
735                n_staged))
736
737    cat("\nLatest commits:\n")
738    lapply(commits(object, n = 5), print)
739
740    invisible(NULL)
741}
742
743## Strip trailing slash or backslash, unless it's the current drive
744## root (/) or a Windows drive, for example, 'c:\'.
745strip_trailing_slash <- function(path) {
746    if (!is.null(path) && grep("^(/|[a-zA-Z]:[/\\\\]?)$", path, invert = TRUE))
747        path <- sub("/?$", "", path)
748    path
749}
750
751##' Workdir of repository
752##'
753##' @template repo-param
754##' @return Character vector with the path of the workdir. If the
755##' repository is bare, \code{NULL} will be returned.
756##' @export
757##' @examples
758##' \dontrun{
759##' ## Create a directory in tempdir
760##' path <- tempfile(pattern="git2r-")
761##' dir.create(path)
762##'
763##' ## Initialize a repository
764##' repo <- init(path)
765##'
766##' ## Get the path of the workdir for repository
767##' workdir(repo)
768##' }
769workdir <- function(repo = ".") {
770    path <- .Call(git2r_repository_workdir, lookup_repository(repo))
771    strip_trailing_slash(path)
772}
773
774##' Find path to repository for any file
775##'
776##' @param path A character vector specifying the path to a file or
777##'     folder
778##' @param ceiling The default is to not use the ceiling argument and
779##'     start the lookup from path and walk across parent
780##'     directories. When ceiling is 0, the lookup is only in
781##'     path. When ceiling is 1, the lookup is in both the path and
782##'     the parent to path.
783##' @return Character vector with path (terminated by a file
784##'     separator) to repository or NULL if this cannot be
785##'     established.
786##' @export
787##' @examples
788##' \dontrun{
789##' ## Initialize a temporary repository
790##' path <- tempfile(pattern="git2r-")
791##' dir.create(path)
792##' repo <- init(path)
793##'
794##' ## Create a user and commit a file
795##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
796##' lines <- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do"
797##' writeLines(lines, file.path(path, "example-1.txt"))
798##' add(repo, "example-1.txt")
799##' commit(repo, "First commit message")
800##'
801##' ## Create a second file. The file is not added for version control
802##' ## in the repository.
803##' dir.create(file.path(path, "example"))
804##' file_2 <- file.path(path, "example/example-2.txt")
805##' writeLines("Not under version control", file_2)
806##'
807##' ## Find the path to the repository using the path to the second file
808##' discover_repository(file_2)
809##'
810##' ## Demonstrate the 'ceiling' argument
811##' wd <- workdir(repo)
812##' dir.create(file.path(wd, "temp"))
813##'
814##' ## Lookup repository in 'file.path(wd, "temp")'. Should return NULL
815##' discover_repository(file.path(wd, "temp"), ceiling = 0)
816##'
817##' ## Lookup repository in parent to 'file.path(wd, "temp")'.
818##' ## Should not return NULL
819##' discover_repository(file.path(wd, "temp"), ceiling = 1)
820##' }
821discover_repository <- function(path = ".", ceiling = NULL) {
822    if (identical(path, "."))
823        path <- getwd()
824    path <- normalizePath(path)
825
826    if (!is.null(ceiling)) {
827        ceiling <- as.integer(ceiling)
828        if (identical(ceiling, 0L)) {
829            ceiling <- dirname(path)
830        } else if (identical(ceiling, 1L)) {
831            ceiling <- dirname(dirname(path))
832        } else {
833            stop("'ceiling' must be either 0 or 1")
834        }
835    }
836
837    path <- .Call(git2r_repository_discover, path, ceiling)
838    strip_trailing_slash(path)
839}
840
841##' Internal utility function to lookup repository for methods
842##'
843##' @param repo repository \code{object} \code{git_repository}, or a
844##'     path to a repository, or \code{NULL}.  If the \code{repo}
845##'     argument is \code{NULL}, the repository is searched for with
846##'     \code{\link{discover_repository}} in the current working
847##'     directory.
848##' @return git_repository
849##' @noRd
850lookup_repository <- function(repo = NULL) {
851    if (is.null(repo) || identical(repo, ".")) {
852        ## Try current working directory
853        repo <- discover_repository(getwd())
854        if (is.null(repo))
855            stop("The working directory is not in a git repository")
856    } else if (inherits(repo, "git_repository")) {
857        return(repo)
858    }
859
860    repository(repo)
861}
862