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##' Add file(s) to index
18##'
19##' @template repo-param
20##' @param path Character vector with file names or shell glob
21##'     patterns that will matched against files in the repository's
22##'     working directory. Each file that matches will be added to the
23##'     index (either updating an existing entry or adding a new
24##'     entry).
25##' @param force Add ignored files. Default is FALSE.
26##' @return invisible(NULL)
27##' @export
28##' @examples
29##' \dontrun{
30##' ## Initialize a repository
31##' path <- tempfile(pattern="git2r-")
32##' dir.create(path)
33##' repo <- init(path)
34##'
35##' ## Create a user
36##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
37##'
38##' ## Create a file
39##' writeLines("a", file.path(path, "a.txt"))
40##'
41##' ## Add file to repository and view status
42##' add(repo, "a.txt")
43##' status(repo)
44##'
45##' ## Add file with a leading './' when the repository working
46##' ## directory is the current working directory
47##' setwd(path)
48##' writeLines("b", file.path(path, "b.txt"))
49##' add(repo, "./b.txt")
50##' status(repo)
51##'
52##' ## Add a file in a sub-folder with sub-folder as the working
53##' ## directory. Create a file in the root of the repository
54##' ## working directory that will remain untracked.
55##' dir.create(file.path(path, "sub_dir"))
56##' setwd("./sub_dir")
57##' writeLines("c", file.path(path, "c.txt"))
58##' writeLines("c", file.path(path, "sub_dir/c.txt"))
59##' add(repo, "c.txt")
60##' status(repo)
61##'
62##' ## Add files with glob expansion when the current working
63##' ## directory is outside the repository's working directory.
64##' setwd(tempdir())
65##' dir.create(file.path(path, "glob_dir"))
66##' writeLines("d", file.path(path, "glob_dir/d.txt"))
67##' writeLines("e", file.path(path, "glob_dir/e.txt"))
68##' writeLines("f", file.path(path, "glob_dir/f.txt"))
69##' writeLines("g", file.path(path, "glob_dir/g.md"))
70##' add(repo, "glob_dir/*txt")
71##' status(repo)
72##'
73##' ## Add file with glob expansion with a relative path when
74##' ## the current working directory is inside the repository's
75##' ## working directory.
76##' setwd(path)
77##' add(repo, "./glob_dir/*md")
78##' status(repo)
79##' }
80add <- function(repo = ".", path = NULL, force = FALSE) {
81    ## Documentation for the pathspec argument in the libgit2 function
82    ## 'git_index_add_all' that git2r use internally:
83    ##
84    ## The pathspec is a list of file names or shell glob patterns
85    ## that will matched against files in the repository's working
86    ## directory. Each file that matches will be added to the index
87    ## (either updating an existing entry or adding a new entry).
88
89    if (is.null(path) || !is.character(path))
90        stop("'path' must be a character vector")
91
92    repo <- lookup_repository(repo)
93    repo_wd <- normalizePath(workdir(repo), winslash = "/")
94    path <- vapply(path, sanitize_path, character(1), repo_wd = repo_wd)
95
96    .Call(git2r_index_add_all, repo, path, isTRUE(force))
97
98    invisible(NULL)
99}
100
101sanitize_path <- function(p, repo_wd) {
102    np <- suppressWarnings(normalizePath(p, winslash = "/"))
103
104    if (!length(grep("/$", repo_wd)))
105        repo_wd <- paste0(repo_wd, "/")
106
107    ## Check if the normalized path is a non-file e.g. a glob.
108    if (!file.exists(np)) {
109        ## Check if the normalized path starts with a leading './'
110        if (length(grep("^[.]/", np))) {
111            nd <- suppressWarnings(normalizePath(dirname(p), winslash = "/"))
112            if (!length(grep("/$", nd)))
113                nd <- paste0(nd, "/")
114            np <- paste0(nd, basename(np))
115        }
116    }
117
118    ## Check if the file is in the repository's working directory,
119    ## else let libgit2 handle this path unmodified.
120    if (!length(grep(paste0("^", repo_wd), np)))
121        return(p)
122
123    ## Change the path to be relative to the repository's working
124    ## directory. Substitute common prefix with ""
125    sub(paste0("^", repo_wd), "", np)
126}
127
128##' Remove files from the working tree and from the index
129##'
130##' @template repo-param
131##' @param path character vector with filenames to remove. Only files
132##'     known to Git are removed.
133##' @return invisible(NULL)
134##' @export
135##' @examples
136##' \dontrun{
137##' ## Initialize a repository
138##' path <- tempfile(pattern="git2r-")
139##' dir.create(path)
140##' repo <- init(path)
141##'
142##' ## Create a user
143##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
144##'
145##' ## Create a file
146##' writeLines("Hello world!", file.path(path, "file-to-remove.txt"))
147##'
148##' ## Add file to repository
149##' add(repo, "file-to-remove.txt")
150##' commit(repo, "First commit message")
151##'
152##' ## Remove file
153##' rm_file(repo, "file-to-remove.txt")
154##'
155##' ## View status of repository
156##' status(repo)
157##' }
158rm_file <- function(repo = ".", path = NULL) {
159    if (is.null(path) || !is.character(path))
160        stop("'path' must be a character vector")
161
162    repo <- lookup_repository(repo)
163
164    if (length(path)) {
165        repo_wd <- workdir(repo)
166        repo_wd <- normalizePath(workdir(repo), winslash = "/")
167        path <- vapply(path, sanitize_path, character(1), repo_wd = repo_wd)
168
169        ## Check that files exists and are known to Git
170        if (!all(file.exists(file.path(repo_wd, path)))) {
171            stop(sprintf("pathspec '%s' did not match any files. ",
172                         path[!file.exists(file.path(repo_wd, path))]))
173        }
174
175        if (any(file.info(file.path(repo_wd, path))$isdir)) {
176            stop(sprintf("pathspec '%s' did not match any files. ",
177                         path[exists(file.path(repo_wd, path))]))
178        }
179
180        s <- status(repo, staged = TRUE, unstaged = TRUE,
181                    untracked = TRUE, ignored = TRUE)
182        if (any(path %in% c(s$ignored, s$untracked))) {
183            stop(sprintf("pathspec '%s' did not match any files. ",
184                         path[path %in% c(s$ignored, s$untracked)]))
185        }
186
187        if (any(path %in% s$staged)) {
188            stop(sprintf("'%s' has changes staged in the index. ",
189                         path[path %in% s$staged]))
190        }
191
192        if (any(path %in% s$unstaged)) {
193            stop(sprintf("'%s' has local modifications. ",
194                         path[path %in% s$unstaged]))
195        }
196
197        ## Remove and stage files
198        lapply(path, function(x) {
199            file.remove(file.path(repo_wd, x))
200            .Call(git2r_index_remove_bypath, repo, x)
201        })
202    }
203
204    invisible(NULL)
205}
206
207##' Remove an index entry corresponding to a file on disk
208##'
209##' @template repo-param
210##' @param path character vector with filenames to remove. The path
211##'     must be relative to the repository's working folder. It may
212##'     exist. If this file currently is the result of a merge
213##'     conflict, this file will no longer be marked as
214##'     conflicting. The data about the conflict will be moved to the
215##'     "resolve undo" (REUC) section.
216##' @return invisible(NULL)
217##' @export
218##' @examples
219##' \dontrun{
220##' ## Initialize a repository
221##' path <- tempfile(pattern="git2r-")
222##' dir.create(path)
223##' repo <- init(path)
224##'
225##' ## Create a user
226##' config(repo, user.name = "Alice", user.email = "alice@@example.org")
227##'
228##' ## Create a file
229##' writeLines("Hello world!", file.path(path, "file-to-remove.txt"))
230##'
231##' ## Add file to repository
232##' add(repo, "file-to-remove.txt")
233##'
234##' ## View status of repository
235##' status(repo)
236##'
237##' ## Remove file
238##' index_remove_bypath(repo, "file-to-remove.txt")
239##'
240##' ## View status of repository
241##' status(repo)
242##' }
243index_remove_bypath <- function(repo = ".", path = NULL) {
244    .Call(git2r_index_remove_bypath, lookup_repository(repo), path)
245    invisible(NULL)
246}
247