1. 개요
이전의 [Project] Tico & Tico Simulator 포스팅에 이어 또 다른 과제를 소개하고자 한다. Open-Source Software Lab (OSSL) 수업의 과제였는데 버전관리시스템(VCS) 에 관한 과제였다.
학습한 Git 의 동작원리와 사용방법을 토대로 해당 프로그램을 제작하라.
하지만 대략적인 과제의 내용은 위와 같았고 나를 포함한 대다수의 학생에게 해당 내용은 다소 충격적이게 다가왔다. OSS 수업은 교수님에 따라 크게 두 분반으로 나뉘었는데 당시 학생들 사이에서 "너네는 Git 배우냐? 우리는 Git 만든다" 와 같은 말을 실소와 함께 주고받는 모습을 기억한다. 하지만 겉보기에 무시무시했던 이 과제는 교수님의 여러 조건들 덕분에 결론적으로 할 만 했던 과제가 되었다. 이 과제 역시 상당히 공을 들였던 과제이므로 내 프로젝트의 일환으로 설명과 해결과정을 상세히 기록하고자 한다.
2. 과제 설명
과제 원문
- Construct a directory backup tool Keep in C programming language
- with Keep, a user can save a series of the snapshot of a target directory
- also, a user can bring back a stored status of the target directory using Keep
- Use file-I/O functions appropriately, and handle various error cases properly
한국어 해석
- 디렉토리 백업 도구 Keep 을 C언어로 개발하라.
- 단, 사용자는 특정 디렉토리에 일련의 스냅샷을 저장할 수 있다.
- 또한, 사용자는 원하는 저장된 디렉토리를 Keep 을 사용해 불러올 수 있다.
- 파일 입출력을 적절히 사용하고 여러 가지 오류에 적절히 대응하도록 구현하라.
Keep 은 교수님께서 교육목적의 버전관리시스템(VCS) 을 구상한 것이다. 교육목적으로 제작되었기 때문에 대개 많이 사용하는 버전관리프로그램 Git 의 하위호환 정도로 생각할 수 있다. 학생들은 교수님께서 제시한 개념을 이해하고 각자의 방법으로 제시된 기능을 수행하는 프로그램을 제작하는 것이 해당 과제의 목표이다.
2.1. Keep
다음은 Keep 에 대한 설명이다.
Keep은 사용자가 대상 디렉토리에 대한 백업 공간을 생성한 후 원하는 디렉토리의 파일을 저장 및 불러오기 할 수 있도록 한다.
2.1.1. 사용가능 명령
Instruction | Description |
---|---|
keep init |
현재 디렉토리에 백업 공간을 만들고 이를 초기화 한다. |
keep track <file or directory> |
명시된 파일 <file> 혹은 명시된 디렉토리 <directory> 에 존재하는 모든 파일을 track 한다. |
keep store "<note>" |
현재 디렉토리의 상태를 저장하고 "note" 의 메시지를 남긴다. |
keep restore "<version>" |
명시된 버전 <version> 의 상태를 불러온다. |
keep versions |
모든 버전의 목록을 출력한다. |
2.1.1.1. 명령어 설정
이름이 keep
인 C언어 기반 실행파일을 실행하려면 keep
이 아닌 ./keep
과 같이 입력해야 한다. 따라서 alias 를 지정하여 keep
을 사용할 수 있도록 하고자 한다.
alias keep="$PWD/keep"
위 명령을 실행파일 keep
이 존재하는 디렉토리 위치의 터미널에서 입력하면 자유롭게 keep
명령어의 사용이 가능해진다.
2.1.2. 디렉토리 구조
- 대상 디렉토리의 백업 공간은
.keep
의 이름을 갖는 숨김 디렉토리이다.- 각
store
명령에 대하여 대상 디렉토리의 복사본에는 고유 버전 번호가 부여된다.
- 고유 번호는 1 부터 시작하여 1씩 증가한다.
.keep
디렉토리는 다음의 파일 및 디렉토리를 갖는다:
tracking-files
: 모든track
상태의 파일의 정보를 목록화한다.latest-version
: 최신 버전 번호를 저장한다. (초깃값 0)<version>
: 명시된 버전의 모든 정보와 파일을 저장한다.
<version>/tracking-files
: 명시된 버전의 모든track
상태의 파일의 정보를 목록화한다.<version>/note
: 해당 버전의"note"
를 저장한다.<version>/target
: 명시된 버전의 대상 디렉토리를 저장한다.
2.1.3. 예제
ls –R
README.md
ex1/hello.c
ex1/main.c
ex1/data/d001
ex1/data/d002
ex2/list1.c
ex2/list2.c
ex2/list3.c
keep init
ls .keep
tracking-files
latest-version
keep track README.md
keep track ex1
keep store "First version"
stored as version 1
ls –R .keep
tracking-files
latest-version
1/tracking-files
1/note
1/target/README.md
1/target/ex1/hello.c
1/target/ex1/main.c
1/target/ex1/data/d001
1/target/ex1/data/d002
keep versions
1 First version
vim README.md
keep untrack ex1/main.c
keep store "Second one."
stored as version 2
keep versions
1 First version
2 Second one.
ls –R .keep/2
2/tracking-files
2/note
2/target/README.md
1/target/ex1/hello.c
1/target/ex1/data/d001
1/target/ex1/data/d002
rm –rf README.md
keep store "Remove README"
stored as version 3
ls .keep
tracking-files
latest-version
1
2
3
keep restore 2
restored as version 2
ls –R
README.md
ex1/hello.c
ex1/data/d001
ex1/data/d002
vim ex1/hello.c
keep store "update hello.c"
stored as version 4
cat .keep/latest-version
4
ls –R .keep/4
4/tracking-files
4/note
4/target/README.md
4/target/ex1/hello.c
4/target/ex1/data/d001
4/target/ex1/data/d002
keep store "backup"
nothing to store
2.1.4.1. 디렉토리 구조
예제는 다음의 디렉토리 구조를 갖는다.
README.md
ex1/hello.c
ex1/main.c
ex1/data/d001
ex1/data/d002
ex2/list1.c
ex2/list2.c
ex2/list3.c
2.1.4.2. 예시입출력
- 교수님께서 주신 입출력 예시
ls -R
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c
ex1/main.c
ex2
ex2/list1.c
ex2/list2.c
ex2/list3.c
- 실제 입출력
ls -R
README.md ex1 ex2
./ex1:
data hello.c main.c
./ex1/data:
d001 d002
./ex2:
list1.c list2.c list3.c
위의 출력값과 같이 교수님께서 주신 입출력 예시와 실제 입출력은 차이가 있다. 따라서 교수님께서 제시하신 출력 형식을 맞추기 위해 다음을 터미널에 입력한다.
function lr() {
if [[ "$1" == "-a" ]]; then
if [[ -n "$2" ]]; then
find "$2" -print | sed -e "s|^$2/||" | grep -v "$1" | sort
else
find . -print | sed -e "s|^\./||" | sort
fi
else
if [[ -n "$1" && ! "$1" =~ ^- ]]; then
find "$1" -print | sed -e "s|^$1/||" | grep -v "$1" | grep -v "^\." | sort
else
find . -print | sed -e "s|^\./||" | grep -v "^\." | sort
fi
fi
}
이제 lr
명령어를 터미널에서 사용할 수 있고 해당 명령어에 대한 설명은 다음와 같다.
lr
명령어
- Parameter
<directory>
: 재귀적으로 파일 및 디렉토리를 출력할 대상 디렉토리- Option
-a
: 숨김파일 출력 여부
2.1.4. 기능
2.1.4.1. init
keep init
- 현재 디렉토리에 빈 파일인
tracking-files
과0
의 문자가 들어있는latest-version
파일을 가지는.keep
디렉토리를 생성한다. .keep
디렉토리가 이미 존재할 경우 오류 메시지를 출력한다.
2.1.4.2. track
keep track <file or directory>
- 명시된 파일
<file>
또는 명시된 디렉토리<directory>
하에 있는 모든 파일 정보를tracking-files
파일에 추가한다.- 단 파일 정보는 파일 위치 주소와 최근 수정 시간을 포함한다.
- 파일이 새롭게 추가될 경우 최근 수정 시간은 0으로 초기화한다.
2.1.4.3. untrack
keep untrack <file or directory>
tracking-files
파일에 명시된 파일 <file>
또는 명시된 디렉토리 <directory>
하에 있는 모든 파일 정보를 삭제한다.
2.1.4.4. versions
keep versions
존재하는 모든 버전의 번호와 메시지 "note"
를 출력한다.
2.1.4.5. store
keep store "note"
- 트래킹하는 파일 중 수정된 파일을 구분한다.
- 각각의 트래킹하는 파일에 대하여 해당 파일의 실제 최근 수정 시간 (
struct stat
의st_mtime
) 과tracking-files
내의 시간 정보를 비교한다. - 파일이 수정되지 않았을 경우,
"noting to update"
를 출력한다.
- 각각의 트래킹하는 파일에 대하여 해당 파일의 실제 최근 수정 시간 (
.keep
디렉토리 아래<version>
디렉토리를 생성한다.target
디렉토리를 생성하여 그 디렉토리 아래 트래킹하는 파일을 저장한다..keep
디렉토리 아래 존재하는tracking-files
을 복사하여.keep/<version>
디렉토리 아래에 붙여 넣는다.note
파일을 생성하여 현재 버전 번호<version>
을 기입한다.
latest-version
에 저장된 최신 버전 번호를1
증가시킨다.
2.1.4.6. restore
keep restore <version>
- 어떤 트래킹하는 파일이
store
또는restore
후에 수정되었는지 확인하고, 수정된 파일이 존재할 경우 해당 명령을 중지한다.- 각 트래킹하는 파일에 대하여 실제 최근 수정 시간과
tracking-files
내의 시간 정보를 비교한다.
- 각 트래킹하는 파일에 대하여 실제 최근 수정 시간과
version
디렉토리에서 각각의 트래킹하는 파일을 복사하고target
디렉토리에 붙여넣는다.- 트래킹하지 않는 파일을 삭제한다.
.keep/tracking-files
파일을 수정한다.
2.2. 과제 요구사항
과제는 다음의 요구사항이 수반된다.
target
디렉토리 아래 hard link 또는 symbolic link 가 존재하지 않는다고 가정한다.- 사용자가 디렉토리를 병렬로 유지하지 않는다고 가정한다.
- 다양한 예외적인 상황에 대해 적절한 오류 메시지를 출력하고 적절하게 처리하도록 해야한다.
- 프로그램을
Makefile
를 사용하여 동작시킬 수 있어야 한다.
3. 구현
3.1. #define
상수
/* PREPROCESSORS */
#define INF_PM INT_MAX
#define PM_LEN 10
Expression | Description | Value |
---|---|---|
INF_PM |
무한의 매개변수를 가짐 | INT_MAX |
PM_LEN |
매개변수 최대 개수 | 10 |
3.2. 열거형 enum
3.2.1. ErrorType
typedef enum {
NO_ERROR, NO_KEEP, KP_EXST,
PM_UNMATCH, STAT_FAIL, FILE_OPN,
NO_UPDT, MT_NOTE, VER_NF,
STORE_FST, ERR_LEN
} ErrorType;
Value | Expression | Description |
---|---|---|
0 |
NO_ERROR |
No error occured |
1 |
NO_KEEP |
Not in a keep directory |
2 |
KP_EXST |
Already in a keep directory |
3 |
PM_UNMATCH |
Unmatched number of parameter |
4 |
STAT_FAIL |
Failed to access stat |
5 |
FILE_OPN |
File open error |
6 |
NO_UPDT |
Nothing to update |
7 |
MT_NOTE |
Note is empty |
8 |
VER_NF |
Version not found |
9 |
STORE_FST |
modified file exists |
10 |
ERR_LEN |
Number of errors |
typedef enum {
INIT, TRACK, UNTRACK, STORE,
RESTORE, VERSIONS, CMD_LEN
} Command;
3.3. 구조체 struct
3.3.1. FileData
typedef struct {
char *filename;
time_t modtime;
} FileData;
파일 정보는 이름과 수정시간을 포함한다.
3.4. 전역변수
3.4.1. pmNums
각 명령어가 필요로 하는 매개변수의 개수가 담긴 정수 배열이다.
- 자료형:
int *
3.4.2. cmds
각 명령어가 담긴 문자열 배열이다.
- 자료형:
char **
3.4.3. errMsg
각 상황에 대한 오류 메시지가 담긴 문자열 배열이다.
- 자료형:
char **
3.4.4. ignore
무시할 파일 정보가 담긴 문자열 배열이다.
- 자료형:
char **
3.4.5. ignoreLen
ignore
배열의 크기이다.
- 자료형:
int
3.4.6. cmdStr
명령어 문자열이다.
- 자료형:
char *
3.4.7. params
명령의 매개변수가 담긴 문자열 배열이다.
- 자료형:
char **
3.4.8. cmd
명령어 번호(Enum
) 이다.
- 자료형:
Command
3.4.9. fileList
전체 파일 목록 정보를 담은 FileData
구조체의 배열이다.
- 자료형:
FileData *
3.4.10. listLen
배열 FileData
의 크기이다.
- 자료형:
int
3.4.11. tracking
트래킹하는 파일 목록 정보를 담은 FileData
구조체의 배열이다.
- 자료형:
FileData *
3.4.12. listLen
배열 tracking
의 크기이다.
- 자료형:
int
3.4.13. latestVersion
최신 버전의 번호를 담는 변수이다.
- 자료형:
unsigned int
3.5. 함수
3.5.1. 문자열-정수 확장 함수
3.5.1.1. intlen()
integer length
주어진 정수에서 숫자(Digit) 의 개수를 계산한다.
- 프로토타입
int intlen(int i);
- 구현부
/// @brief Calculates the number of digits in a given integer when it is converted to a string
/// @param i The integer to calculate the number of digits for
/// @return The number of digits in the integer as an integer value
int intlen(int i) { return 1 + (i ? (int) log10(i) : 1); }
- 매개변수
Type | Expression | Description |
---|---|---|
int |
i |
숫자 개수를 계산하고자 하는 정수 |
- 반환값
Type | Description |
---|---|
int |
정수 i 의 숫자(Digit) 개수 |
- 예시
intlen(3); // 1
intlen(50); // 2
intlen(1234); // 4
3.5.1.2. min()
minimum
주어진 두 수 중 더 작은 수를 반환한다.
- 프로토타입
int min(int x, int y);
- 구현부
/// @brief Returns the smaller of two integers
/// @param x The first integer to compare
/// @param y The second integer to compare
/// @return The smaller of the two integers
int min(int x, int y) { return x < y ? x : y; }
- 매개변수
Type | Expression | Description |
---|---|---|
int |
x |
비교하고자 하는 첫 번째 정수 |
int |
y |
비교하고자 하는 두 번째 정수 |
- 반환값
Type | Description |
---|---|
int |
두 정수 x , y 중 작은 정수 |
- 예시
min(2, 5) // 2
min(9, 5) // 5
3.5.1.3. max()
maximum
주어진 두 수 중 더 큰 수를 반환한다.
- 프로토타입
int max(int x, int y);
- 구현부
/// @brief Returns the bigger of two integers
/// @param x The first integer to compare
/// @param y The second integer to compare
/// @return The bigger of the two integers
int max(int x, int y) { return x > y ? x : y; }
- 매개변수
Type | Expression | Description |
---|---|---|
int |
x |
비교하고자 하는 첫 번째 정수 |
int |
y |
비교하고자 하는 두 번째 정수 |
- 반환값
Type | Description |
---|---|
int |
두 정수 x , y 중 큰 정수 |
- 예시
max(2, 5) // 5
max(9, 5) // 9
3.5.2. 파일관리 함수
3.5.2.1. loadFileList()
파일 목록을 불러온다.
- 프로토타입
void loadFileList(char *path);
- 구현부
/// @brief Loads and lists files or directories based on the given path
/// @param path The path to a file or directory
void loadFileList(char *path) {
struct stat st;
if (stat(path, &st)) terminate(STAT_FAIL);
if (S_ISREG(st.st_mode)) listFiles(path);
else if (S_ISDIR(st.st_mode)) listDirs(path);
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
path |
파일 또는 디렉토리의 주소 |
3.5.2.2. listFiles()
파일 목록을 불러온다.
- 프로토타입
void listFiles(char *filepath);
- 구현부
/// @brief Adds a file to the fileList if it should not be ignored
/// @param filepath The path to the file to be listed
void listFiles(char *filepath) {
struct stat st;
stat(filepath, &st);
if (!beIgnored(filepath)) {
char *newStr = (char *) malloc(strlen(filepath) + 1);
strncpy(newStr, filepath, strlen(filepath) + 1);
fileList[listLen].filename = newStr + 2 * !strncmp(newStr, "./", 2);
fileList[listLen++].modtime = st.st_mtime;
}
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
filepath |
목록화할 파일의 주소 |
3.5.2.3. listDirs()
디렉토리 목록을 불러온다.
- 프로토타입
void listDirs(char *dirpath);
- 구현부
/// @brief Lists the files and subdirectories within a directory
/// @param dirpath The path to the directory to list
void listDirs(char *dirpath) {
DIR * dir = opendir(dirpath);
if (dir == 0x0) return;
for (struct dirent *i = readdir(dir); i; i = readdir(dir)) {
if (i->d_type != DT_DIR && i->d_type != DT_REG) continue;
char *filepath = (char *) malloc(strlen(dirpath) + 1 + strlen(i->d_name) + 1);
strcpy(filepath, dirpath);
strcpy(filepath + strlen(dirpath), "/");
strcpy(filepath + strlen(dirpath) + 1, i->d_name);
bool pass = false;
switch (i->d_type) {
case DT_DIR:
pass |= !strcmp(i->d_name, ".");
pass |= !strcmp(i->d_name, "..");
pass |= beIgnored(filepath);
if (!pass) listDirs(filepath);
break;
case DT_REG: listFiles(filepath); break;
default: break;
}
free(filepath);
}
closedir(dir);
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
dirpath |
목록화할 디렉토리의 주소 |
3.5.2.4. getDir()
주어진 주소에서 디렉토리 부분만을 추출한다.
- 프로토타입
char *getDir(char *path);
- 구현부
/// @brief Extracts the directory part of a given path
/// @param path The path to extract the directory from
/// @return The directory part of the path
char *getDir(char *path) {
char *dir = malloc(strlen(path) * sizeof(char));
char *lastSlash = strrchr(path, '/');
if (lastSlash != NULL) {
size_t length = lastSlash - path + 1;
strncpy(dir, path, length);
dir[length] = '\0';
} else dir[0] = '\0';
return dir;
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
path |
디렉토리 부분만 추출할 주소 |
- 반환값
Type | Description |
---|---|
char * |
추출된 디렉토리 부분 문자열 |
- 예시
getDir("keep/ex1/a.txt"); // "keep/ex1/"
getDir("keep/ex1/b"); // "keep/ex1/"
3.5.2.5. copyFile()
src
주소의 파일을 복사하여 des
에 붙여넣는다.
- 프로토타입
void copyFile(char *des, char *src);
- 구현부
/// @brief Copies the contents of one file to another
/// @param des The destination directory path
/// @param src The source file path
void copyFile(char *des, char *src) {
char *desdir = getDir(des);
mkdir(desdir, 0755);
struct stat st;
stat(src, &st);
FILE* srcfp = fopen(src, "rb");
FILE* desfp = fopen(des, "wb");
if (!srcfp || !desfp) terminate(FILE_OPN);
char buf[BUFSIZ];
size_t read;
while ((read = fread(buf, 1, sizeof(buf), srcfp)) > 0) {
fwrite(buf, 1, read, desfp);
}
free(desdir);
fclose(srcfp);
fclose(desfp);
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
des |
붙여넣을 디렉토리 주소 |
char * |
src |
복사할 파일 주소 |
3.5.3. Keep 기본 함수
3.5.3.1. terminate()
오류 메시지를 출력하고 프로그램을 종료한다.
- 프로토타입
void terminate(ErrorType err);
- 구현부
/// @brief Terminates the program with an error message
/// @param err The error type
void terminate(ErrorType err) {
fprintf(stderr, "[ERROR] %s\n", errMsg[err]);
exit(err);
}
- 매개변수
Type | Expression | Description |
---|---|---|
ErrorType |
err |
명시할 오류의 종류 |
3.5.3.2. getPmNum()
명령 문자열에서 매개변수의 개수를 반환한다.
- 프로토타입
int getPmNum(char *cmdStr);
- 구현부
/// @brief Gets the number of parameters for a given command
/// @param cmdStr The command string
/// @return The number of parameters for the command
int getPmNum(char *cmdStr) {
for (int i = 0; i < CMD_LEN; i++)
if (!strcmp(cmds[i], cmdStr)) { cmd = i; return pmNums[i]; }
return -1;
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
cmdStr |
명령 문자열 |
- 반환값
Type | Description |
---|---|
int |
명령의 매개변수 개수 |
- 예시
getPmNum("init"); // 0
getPmNum("track"); // 2147483647
getPmNum("store"); // 1
3.5.3.3. getIgnores()
.keepignore
파일의 내용으로부터 무시할 파일 정보를 파악하여 ignore
배열에 각 파일의 이름을, ignoreLen
에 해당 파일의 개수를 저장한다.
- 프로토타입
void getIgnores();
- 구현부
/// @brief Loads ignore patterns from a file
/// This function reads ignore patterns from the `.keepignore` file and other default patterns, and stores them in the `ignore` array
void getIgnores() {
FILE *file = fopen(".keepignore", "r");
char **arr = (char **) malloc(1000 * sizeof(char *));
int count = 0;
char line[1000];
if (file) {
while (fgets(line, 1000, file)) {
if (line[strlen(line) - 1] == '\n')
line[strlen(line) - 1] = '\0';
char *newStr = (char *) malloc(strlen(line) + 2);
sprintf(newStr, "./%s", line);
arr[count++] = newStr;
}
fclose(file);
}
arr[count++] = "./.keep";
arr[count++] = "./.git";
arr[count++] = "./keep.c";
arr[count++] = "./keep";
ignore = arr;
ignoreLen = count;
}
- 매개변수
Type | Expression | Description |
---|---|---|
char ** |
params |
출력할 매개변수 배열 |
3.5.3.4. beIgnored()
파일의 무시 여부를 판단한다.
- 프로토타입
bool beIgnored(char *path);
- 구현부
/// @brief Checks if a path should be ignored based on ignore patterns
/// @param path The path to check
/// @return True if the path should be ignored, otherwise false
bool beIgnored(char *path) {
for (int i = 0; i < ignoreLen; i++) {
char *temp = (char *) malloc((strlen(path) + 2) * sizeof(char));
if (strncmp(path, "./", 2)) sprintf(temp, "./%s", path);
else strcpy(temp, path);
struct stat st;
stat(ignore[i], &st);
bool cond = !strncmp(ignore[i], temp, strlen(ignore[i]));
if (temp[strlen(ignore[i]) - 1] != '/') cond = !strcmp(ignore[i], temp);
if (cond) return true;
free(temp);
}
return false;
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
path |
무시 여부를 판단할 파일의 주소 |
- 반환값
Type | Description |
---|---|
bool |
명시된 주소의 파일의 무시 여부 |
3.5.3.5. getCommand()
argv
에서 명령어와 매개변수를 추출하여 cmdStr
과 params
에 저장한다.
- 프로토타입
void getCommand(int argc, char **argv);
- 구현부
/// @brief Parses command line arguments and sets the command and parameters
/// @param argc The argument count
/// @param argv The argument vector
void getCommand(int argc, char **argv) {
for (int i = 0; i < PM_LEN; i++) {
params[i] = (char *) malloc(100);
params[i][0] = '\0';
}
strcpy(cmdStr, argv[1]);
int paramCnt = getPmNum(cmdStr);
if (paramCnt == INF_PM) {
if (argc < 3) terminate(PM_UNMATCH);
}
else if (argc != paramCnt + 2) terminate(PM_UNMATCH);
if (paramCnt) {
for (int i = 0; i < min(argc - 2, PM_LEN); i++) {
if (argv[2 + i][strlen(argv[2 + i]) - 1] == '/') {
argv[2 + i][strlen(argv[2 + i]) - 1] = '\0';
strcpy(params[i], argv[2 + i]);
}
if (strncmp(argv[2 + i], "./", 2))
strcpy(params[i], argv[2 + i]);
else strcpy(params[i], argv[2 + i] + 2);
}
}
}
- 매개변수
Type | Expression | Description |
---|---|---|
int |
argc |
매개변수의 개수 |
char ** |
argv |
매개변수 벡터 |
3.5.3.6. freeCommand()
params
배열의 할당을 해제한다.
- 프로토타입
void freeCommand();
- 구현부
/// @brief Frees the memory allocated for command parameters
void freeCommand() {
for (int i = 0; i < PM_LEN; i++) free(params[i]);
}
3.5.3.7. loadTrackingFiles()
트래킹하는 파일 정보를 .keep/tracking-files
파일로부터 불러온다.
- 프로토타입
void loadTrackingFiles();
- 구현부
/// @brief Loads tracking files from a file into the tracking array
void loadTrackingFiles() {
FILE *tffp = fopen(".keep/tracking-files", "r");
if (!tffp) return;
char line[1000];
FileData *arr = malloc(1000 * sizeof(FileData));
int count = 0;
while (fgets(line, 1000, tffp)) {
if (line[strlen(line) - 1] == '\n')
line[strlen(line) - 1] = '\0';
char *name = strtok(line, " ");
char *time = strtok(NULL, " ");
arr[count].filename = (char *) malloc(strlen(name) * sizeof(char));
strcpy(arr[count].filename, name);
arr[count++].modtime = (time_t) atoi(time);
}
tracking = arr;
trackLen = count;
fclose(tffp);
}
3.5.3.8. saveTrackingFiles()
트래킹하는 파일 정보를 .keep/tracking-files
파일에 저장한다.
- 프로토타입
void saveTrackingFiles();
- 구현부
/// @brief Saves tracking files from the tracking array into a file
void saveTrackingFiles() {
FILE *tffp = fopen(".keep/tracking-files", "w");
if (!tffp) return;
for (int i = 0; i < trackLen; i++)
fprintf(tffp, "%s %lu\n", tracking[i].filename, tracking[i].modtime);
fclose(tffp);
}
3.5.3.9. loadLatestVersion()
최신 버전의 번호를 .keep/latest-version
파일에서 불러온다.
- 프로토타입
void loadLatestVersion();
- 구현부
/// @brief Loads the latest version number from a file
void loadLatestVersion() {
FILE *fp = fopen(".keep/latest-version", "r");
char versionStr[100];
fscanf(fp, "%s", versionStr);
latestVersion = (unsigned int) atoi(versionStr);
fclose(fp);
}
3.5.3.10. saveLatestVersion()
변수 latestVersion
값에 1
을 더하여 .keep/latest-version
파일에 저장한다.
- 프로토타입
void saveLatestVersion();
- 구현부
/// @brief Saves the latest version number to a file, incrementing it by one
void saveLatestVersion() {
FILE *fp = fopen(".keep/latest-version", "w");
fprintf(fp, "%d", latestVersion + 1);
fclose(fp);
}
3.5.3.11. saveNote()
현재 버전 <version>
에 대하여 .keep/<version + 1>/note
파일에 msg
문자열을 저장한다.
- 프로토타입
void saveNote(char *msg);
- 구현부
/// @brief Saves a note for the current version
/// @param msg The note message to save
void saveNote(char *msg) {
int verlen = strlen(".keep//note") + intlen(latestVersion + 1);
char path[verlen + 1];
sprintf(path, ".keep/%d/note", latestVersion + 1);
FILE *fp = fopen(path, "w");
if (!fp) terminate(FILE_OPN);
fprintf(fp, "%s", msg);
fclose(fp);
}
- 매개변수
Type | Expression | Description |
---|---|---|
char * |
msg |
저장할 메시지 ("note" ) |
3.5.4. Keep 주요 함수
3.5.4.1. init()
keep init
명령에 대응하는 함수이다.
- 프로토타입
void init();
- 구현부
/// @brief Initializes the .keep directory and files if they do not exist
void init() {
struct stat st;
bool alreadExist = !stat(".keep", &st) && S_ISDIR(st.st_mode);
if (alreadExist) terminate(KP_EXST);
else {
int result = mkdir(".keep", 0755);
FILE *lvfp = fopen(".keep/latest-version", "w");
FILE *tffp = fopen(".keep/tracking-files", "w");
fprintf(lvfp, "0");
fclose(tffp);
fclose(lvfp);
}
}
3.5.4.2. track()
keep track
명령에 대응하는 함수이다.
- 프로토타입
void track();
- 구현부
/// @brief Tracks the specified files by adding them to the tracking list
void track() {
for (int i = 0; i < PM_LEN; i++) {
if (!strcmp(params[i], "")) break;
loadFileList(params[i]);
}
loadTrackingFiles();
for (int i = 0; i < listLen; i++) {
bool isExist = false;
for (int j = 0; j < trackLen; j++) {
if (!strcmp(fileList[i].filename, tracking[j].filename)) {
if (!tracking[j].modtime) tracking[j].modtime = 0;
isExist = true; break;
}
}
if (!isExist) {
tracking[trackLen].filename = fileList[i].filename;
tracking[trackLen++].modtime = 0;
}
}
saveTrackingFiles();
}
3.5.4.3. untrack()
keep untrack
명령에 대응하는 함수이다.
- 프로토타입
void untrack();
- 구현부
/// @brief Untracks the specified files by removing them from the tracking list
void untrack() {
for (int i = 0; i < PM_LEN; i++) {
if (!strcmp(params[i], "")) break;
loadFileList(params[i]);
}
loadTrackingFiles();
FileData *tempTracking = (FileData *) malloc(1000 * sizeof(FileData));
int count = 0;
for (int i = 0; i < trackLen; i++) {
bool isExist = false;
for (int j = 0; j < listLen; j++) {
if (strcmp(tracking[i].filename, fileList[j].filename)) continue;
isExist = true; break;
}
if (!isExist) {
tempTracking[count].filename = tracking[i].filename;
tempTracking[count++].modtime = tracking[i].modtime;
}
}
tracking = tempTracking;
trackLen = count;
saveTrackingFiles();
}
3.5.4.4. store()
keep store
명령에 대응하는 함수이다.
- 프로토타입
void store();
- 구현부
/// @brief Stores the tracked files into the .keep directory as a new version
void store() {
if (!strlen(params[0])) terminate(MT_NOTE);
loadLatestVersion();
loadTrackingFiles();
FileData *arr = (FileData *) malloc(trackLen * sizeof(FileData));
int count = 0;
bool update = false;
for (int i = 0; i < trackLen; i++) {
struct stat st;
bool exist = !stat(tracking[i].filename, &st);
if (!exist) { update = true; continue; }
update |= !tracking[i].modtime || tracking[i].modtime < st.st_mtime;
arr[count].filename = malloc((strlen(tracking[i].filename) + 1) * sizeof(char));
strcpy(arr[count].filename, tracking[i].filename);
arr[count++].modtime = tracking[i].modtime;
}
tracking = arr;
trackLen = count;
if (!update) terminate(NO_UPDT);
int verlen = intlen(latestVersion + 1);
char *desdir = (char *) malloc((strlen(".keep/") + verlen) * sizeof(char));
sprintf(desdir, ".keep/%d", latestVersion + 1);
mkdir(desdir, 0755);
char *tgtdir = (char *) malloc((strlen(".keep//target") + verlen) * sizeof(char));
sprintf(tgtdir, ".keep/%d/target", latestVersion + 1);
mkdir(tgtdir, 0755);
for (int i = 0; i < trackLen; i++) {
int slen = strlen(tgtdir) + strlen(tracking[i].filename);
char *target = (char *) malloc(1000 * sizeof(char));
sprintf(target, "%s/%s", tgtdir, tracking[i].filename);
copyFile(target, tracking[i].filename);
free(target);
}
for (int i = 0; i < trackLen; i++) {
struct stat st;
stat(tracking[i].filename, &st);
tracking[i].modtime = st.st_mtime;
}
printf("stored as version %d\n", latestVersion + 1);
saveTrackingFiles();
char *src = ".keep/tracking-files";
char *des = (char *) malloc((strlen(src) + verlen + 1) * sizeof(char));
sprintf(des, "%s/tracking-files", desdir);
copyFile(des, src);
saveNote(params[0]);
saveLatestVersion();
free(desdir);
free(des);
}
3.5.4.5. restore()
keep restore
명령에 대응하는 함수이다.
- 프로토타입
void restore();
- 구현부
/// @brief Restores the tracked files to a specified version
/// @param version The version number to restore
void restore() {
loadLatestVersion();
int ver = atoi(params[0]);
if (ver < 1 || ver > latestVersion) terminate(VER_NF);
loadFileList(".");
loadTrackingFiles();
bool modified = false;
for (int i = 0; i < trackLen; i++) {
struct stat st;
stat(tracking[i].filename, &st);
modified |= tracking[i].modtime < st.st_mtime;
}
if (modified) terminate(STORE_FST);
char name[50];
sprintf(name, ".keep/%d/tracking-files", ver);
copyFile(".keep/tracking-files", name);
loadTrackingFiles();
for (int i = 0; i < listLen; i++) {
char *dir = getDir(fileList[i].filename);
remove(fileList[i].filename); remove(dir);
}
for (int i = 0; i < trackLen; i++) {
sprintf(name, ".keep/%d/target/%s", ver, tracking[i].filename);
struct stat st1, st2;
bool exist = !stat(name, &st1);
if (!exist) continue;
copyFile(tracking[i].filename, name);
stat(tracking[i].filename, &st2);
tracking[i].modtime = st2.st_mtime;
}
saveTrackingFiles();
printf("restored as version %d\n", ver);
}
3.5.4.6. versions()
keep versions
명령에 대응하는 함수이다.
- 프로토타입
void versions();
- 구현부
/// @brief Displays all stored versions and their notes
void versions() {
loadLatestVersion();
for (int i = 0; i < latestVersion; i++) {
char name[50], note[50];
sprintf(name, ".keep/%d/note", i + 1);
FILE *fp = fopen(name, "r");
fscanf(fp, "%[^\n]", note);
printf("%-2d %s\n", i + 1, note);
fclose(fp);
}
}
4. 실행
4.1. 예제
lr
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c
ex1/main.c
ex2
ex2/list1.c
ex2/list2.c
ex2/list3.c
keep init
lr .keep
latest-version
tracking-files
keep track README.md
keep track ex1
keep store "First version"
stored as version 1
lr .keep
1
1/note
1/target
1/target/README.md
1/target/ex1
1/target/ex1/data
1/target/ex1/data/d001
1/target/ex1/data/d002
1/target/ex1/hello.c
1/target/ex1/main.c
1/tracking-files
latest-version
tracking-files
keep versions
1 First version
vim README.md
keep untrack ex1/main.c
keep store "Second one."
stored as version 2
keep versions
1 First version
2 Second one.
lr .keep/2
note
target
target/README.md
target/ex1
target/ex1/data
target/ex1/data/d001
target/ex1/data/d002
target/ex1/hello.c
tracking-files
rm README.md
keep store "Remove README"
stored as version 3
ls .keep
1 3 tracking-files
2 latest-version
keep restore 2
restored as version 2
lr
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c
vim ex1/hello.c
keep store "update hello.c"
stored as version 4
cat .keep/latest-version
4
lr .keep/4
note
target
target/README.md
target/ex1
target/ex1/data
target/ex1/data/d001
target/ex1/data/d002
target/ex1/hello.c
tracking-files
keep store "backup"
[ERROR] nothing to update
2.1.3. 와 비교해보면 일부 출력 형식만 개선하였고 내용은 같은 것을 확인할 수 있다.
4.2. 시나리오
초기 target
디렉토리 구조
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c
ex1/main.c
ex2
ex2/list1.c
ex2/list2.c
ex2/list3.c
- init
keep init
.keep
디렉토리 구조
latest-version
tracking-files
README.md
파일 track
keep track README.md
.keep/tracking-files
README.md 0
- 모든 파일 track
keep track .
.keep/tracking-files
README.md 0
ex1/hello.c 0
ex1/main.c 0
ex1/data/d001 0
ex1/data/d002 0
ex2/list3.c 0
ex2/list2.c 0
ex2/list1.c 0
ex2
디렉토리 이하 존재하는 모든 파일 untrack
keep untrack ex2
.keep/tracking-files
README.md 0
ex1/hello.c 0
ex1/main.c 0
ex1/data/d001 0
ex1/data/d002 0
"first store"
note 와 함께 store
keep store "first store"
stored as version 1
.keep
디렉토리 구조
1
1/note
1/target
1/target/README.md
1/target/ex1
1/target/ex1/data
1/target/ex1/data/d001
1/target/ex1/data/d002
1/target/ex1/hello.c
1/target/ex1/main.c
1/tracking-files
latest-version
tracking-files
.keep/latest-version
1
.keep/tracking-files
README.md 1722100812
ex1/hello.c 1722100812
ex1/main.c 1722100812
ex1/data/d001 1722100812
ex1/data/d002 1722100812
.keep/1/note
first store
- 파일 변경 (
hello.c
수정,main.c
삭제)
vim ex1/hello.c
$ rm ex1/main.c
target
디렉토리 구조
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c [Modified]
ex2
ex2/list1.c
ex2/list2.c
ex2/list3.c
"second store"
note 와 함께 store
keep store "second store"
stored as version 2
.keep
디렉토리 구조
1
1/note
1/target
1/target/README.md
1/target/ex1
1/target/ex1/data
1/target/ex1/data/d001
1/target/ex1/data/d002
1/target/ex1/hello.c
1/target/ex1/main.c
1/tracking-files
2
2/note
2/target
2/target/README.md
2/target/ex1
2/target/ex1/data
2/target/ex1/data/d001
2/target/ex1/data/d002
2/target/ex1/hello.c
2/tracking-files
latest-version
tracking-files
.keep/latest-version
2
.keep/tracking-files
README.md 1722100812
ex1/hello.c 1722100941
ex1/data/d001 1722100812
ex1/data/d002 1722100812
.keep/2/note
second store
- 버전 정보 출력
keep versions
1 first store
2 second store
- 버전 1 restore
keep restore 1
restored as version 1
target
디렉토리 구조
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c
ex1/main.c
이때 ex2
디렉토리가 완전히 사라지는 것은 트래킹되어 있지 않기 때문이며 의도된 상황이다.
.keep/tracking-files
README.md 1722101021
ex1/hello.c 1722101021
ex1/main.c 1722101021
ex1/data/d001 1722101021
ex1/data/d002 1722101021
- 버전 2 restore
keep restore 2
restored as version 2
target
디렉토리 구조
README.md
ex1
ex1/data
ex1/data/d001
ex1/data/d002
ex1/hello.c
.keep/tracking-files
README.md 1722101117
ex1/hello.c 1722101117
ex1/data/d001 1722101117
ex1/data/d002 1722101117
5. 오류 처리
.keep
디렉토리가 존재하지 않을 때init
명령을 제외한 명령어를 입력할 경우
keep track
[ERROR] not in a keep directory
.keep
디렉토리가 존재할 때init
명령어를 입력할 경우
keep init
[ERROR] already in a keep directory
- 명령어에 해당하는 매개변수의 개수가 잘못 지정된 경우
keep store "note1" "note2"
[ERROR] unmatched number of parameter
- 잘못된 혹은 없는 파일의
stat
에 접근할 경우
keep track <file>
[ERROR] failed to access stat
- 파일 열기에 실패했을 경우
[ERROR] file open error
- 변경 사항 없이 store 명령을 실행할 경우
keep store "note"
[ERROR] nothing to update
- 빈 문자열의
note
와 함께 store 명령을 실행할 경우
keep store ""
[ERROR] note is empty
- 없는 버전의 번호와 함께 restore 명령을 실행할 경우
keep restore <version>
[ERROR] version not found
- 변경 사항이 존재할 때 restore 명령을 실행할 경우
keep restore <version>
[ERROR] modified file exists
6. 전체코드
전체코드는 아래 링크에서 확인할 수 있다.
'Project' 카테고리의 다른 글
[Project] Tico & Tico Simulator (0) | 2024.06.06 |
---|---|
[Project] 3D Renderer (0) | 2024.05.30 |