/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java

https://bitbucket.org/nbargnesi/idea · Java · 510 lines · 375 code · 45 blank · 90 comment · 77 complexity · 7ccbdfff0c8498fe4676c9728582ddf3 MD5 · raw file

  1. /*
  2. * Copyright 2000-2011 JetBrains s.r.o.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package git4idea.repo;
  17. import com.intellij.openapi.application.ApplicationManager;
  18. import com.intellij.openapi.diagnostic.Logger;
  19. import com.intellij.openapi.util.io.FileUtil;
  20. import com.intellij.openapi.vfs.VirtualFile;
  21. import com.intellij.openapi.vfs.encoding.EncodingManager;
  22. import com.intellij.util.Processor;
  23. import com.intellij.vcsUtil.VcsUtil;
  24. import git4idea.GitBranch;
  25. import git4idea.branch.GitBranchesCollection;
  26. import org.jetbrains.annotations.NonNls;
  27. import org.jetbrains.annotations.NotNull;
  28. import org.jetbrains.annotations.Nullable;
  29. import java.io.BufferedReader;
  30. import java.io.File;
  31. import java.io.FileReader;
  32. import java.io.IOException;
  33. import java.nio.charset.Charset;
  34. import java.util.HashMap;
  35. import java.util.HashSet;
  36. import java.util.Map;
  37. import java.util.Set;
  38. import java.util.concurrent.Callable;
  39. import java.util.concurrent.atomic.AtomicReference;
  40. import java.util.regex.Matcher;
  41. import java.util.regex.Pattern;
  42. /**
  43. * Reads information about the Git repository from Git service files located in the {@code .git} folder.
  44. * NB: works with {@link java.io.File}, i.e. reads from disk. Consider using caching.
  45. * Throws a {@link GitRepoStateException} in the case of incorrect Git file format.
  46. * @author Kirill Likhodedov
  47. */
  48. class GitRepositoryReader {
  49. private static final Logger LOG = Logger.getInstance(GitRepositoryReader.class);
  50. private static Pattern BRANCH_PATTERN = Pattern.compile("ref: refs/heads/(\\S+)"); // branch reference in .git/HEAD
  51. // this format shouldn't appear, but we don't want to fail because of a space
  52. private static Pattern BRANCH_WEAK_PATTERN = Pattern.compile(" *(ref:)? */?refs/heads/(\\S+)");
  53. private static Pattern COMMIT_PATTERN = Pattern.compile("[0-9a-fA-F]+"); // commit hash
  54. @NonNls private static final String REFS_HEADS_PREFIX = "refs/heads/";
  55. @NonNls private static final String REFS_REMOTES_PREFIX = "refs/remotes/";
  56. private static final int IO_RETRIES = 3; // number of retries before fail if an IOException happens during file read.
  57. private final File myGitDir; // .git/
  58. private final File myHeadFile; // .git/HEAD
  59. private final File myRefsHeadsDir; // .git/refs/heads/
  60. private final File myRefsRemotesDir; // .git/refs/remotes/
  61. private final File myPackedRefsFile; // .git/packed-refs
  62. GitRepositoryReader(@NotNull File gitDir) {
  63. myGitDir = gitDir;
  64. assertFileExists(myGitDir, ".git directory not found in " + gitDir);
  65. myHeadFile = new File(myGitDir, "HEAD");
  66. assertFileExists(myHeadFile, ".git/HEAD file not found in " + gitDir);
  67. myRefsHeadsDir = new File(new File(myGitDir, "refs"), "heads");
  68. myRefsRemotesDir = new File(new File(myGitDir, "refs"), "remotes");
  69. myPackedRefsFile = new File(myGitDir, "packed-refs");
  70. }
  71. @NotNull
  72. GitRepository.State readState() {
  73. if (isMergeInProgress()) {
  74. return GitRepository.State.MERGING;
  75. }
  76. if (isRebaseInProgress()) {
  77. return GitRepository.State.REBASING;
  78. }
  79. Head head = readHead();
  80. if (!head.isBranch) {
  81. return GitRepository.State.DETACHED;
  82. }
  83. return GitRepository.State.NORMAL;
  84. }
  85. /**
  86. * Finds current revision value.
  87. * @return The current revision hash, or <b>{@code null}</b> if current revision is unknown - it is the initial repository state.
  88. */
  89. @Nullable
  90. String readCurrentRevision() {
  91. final Head head = readHead();
  92. if (!head.isBranch) { // .git/HEAD is a commit
  93. return head.ref;
  94. }
  95. // look in /refs/heads/<branch name>
  96. File branchFile = null;
  97. for (Map.Entry<String, File> entry : readLocalBranches().entrySet()) {
  98. if (entry.getKey().equals(head.ref)) {
  99. branchFile = entry.getValue();
  100. }
  101. }
  102. if (branchFile != null) {
  103. return readBranchFile(branchFile);
  104. }
  105. // finally look in packed-refs
  106. return findBranchRevisionInPackedRefs(head.ref);
  107. }
  108. /**
  109. * If the repository is on branch, returns the current branch
  110. * If the repository is being rebased, returns the branch being rebased.
  111. * In other cases of the detached HEAD returns {@code null}.
  112. */
  113. @Nullable
  114. GitBranch readCurrentBranch() {
  115. Head head = readHead();
  116. if (head.isBranch) {
  117. String branchName = head.ref;
  118. String hash = readCurrentRevision(); // TODO make this faster, because we know the branch name
  119. return new GitBranch(branchName, hash == null ? "" : hash, true, false);
  120. }
  121. if (isRebaseInProgress()) {
  122. GitBranch branch = readRebaseBranch("rebase-apply");
  123. if (branch == null) {
  124. branch = readRebaseBranch("rebase-merge");
  125. }
  126. return branch;
  127. }
  128. return null;
  129. }
  130. /**
  131. * Reads {@code .git/rebase-apply/head-name} or {@code .git/rebase-merge/head-name} to find out the branch which is currently being rebased,
  132. * and returns the {@link GitBranch} for the branch name written there, or null if these files don't exist.
  133. */
  134. @Nullable
  135. private GitBranch readRebaseBranch(@NonNls String rebaseDirName) {
  136. File rebaseDir = new File(myGitDir, rebaseDirName);
  137. if (!rebaseDir.exists()) {
  138. return null;
  139. }
  140. final File headName = new File(rebaseDir, "head-name");
  141. if (!headName.exists()) {
  142. return null;
  143. }
  144. String branchName = tryLoadFile(headName, calcEncoding(headName)).trim();
  145. if (branchName.startsWith(REFS_HEADS_PREFIX)) {
  146. branchName = branchName.substring(REFS_HEADS_PREFIX.length());
  147. }
  148. return new GitBranch(branchName, true, false);
  149. }
  150. private boolean isMergeInProgress() {
  151. File mergeHead = new File(myGitDir, "MERGE_HEAD");
  152. return mergeHead.exists();
  153. }
  154. private boolean isRebaseInProgress() {
  155. File f = new File(myGitDir, "rebase-apply");
  156. if (f.exists()) {
  157. return true;
  158. }
  159. f = new File(myGitDir, "rebase-merge");
  160. return f.exists();
  161. }
  162. /**
  163. * Reads the {@code .git/packed-refs} file and tries to find the revision hash for the given reference (branch actually).
  164. * @param ref short name of the reference to find. For example, {@code master}.
  165. * @return commit hash, or {@code null} if the given ref wasn't found in {@code packed-refs}
  166. */
  167. @Nullable
  168. private String findBranchRevisionInPackedRefs(final String ref) {
  169. if (!myPackedRefsFile.exists()) {
  170. return null;
  171. }
  172. return tryOrThrow(new Callable<String>() {
  173. @Override
  174. public String call() throws Exception {
  175. BufferedReader reader = null;
  176. try {
  177. reader = new BufferedReader(new FileReader(myPackedRefsFile));
  178. String line;
  179. while ((line = reader.readLine()) != null) {
  180. final AtomicReference<String> hashRef = new AtomicReference<String>();
  181. parsePackedRefsLine(line, new PackedRefsLineResultHandler() {
  182. @Override
  183. public void handleResult(String hash, String branchName) {
  184. if (hash == null || branchName == null) {
  185. return;
  186. }
  187. if (branchName.endsWith(ref)) {
  188. hashRef.set(hash);
  189. }
  190. }
  191. });
  192. if (hashRef.get() != null) {
  193. return hashRef.get();
  194. }
  195. }
  196. return null;
  197. }
  198. finally {
  199. if (reader != null) {
  200. reader.close();
  201. }
  202. }
  203. }
  204. }, myPackedRefsFile);
  205. }
  206. /**
  207. * @return the list of local branches in this Git repository.
  208. * key is the branch name, value is the file.
  209. */
  210. private Map<String, File> readLocalBranches() {
  211. final Map<String, File> branches = new HashMap<String, File>();
  212. if (!myRefsHeadsDir.exists()) {
  213. return branches;
  214. }
  215. FileUtil.processFilesRecursively(myRefsHeadsDir, new Processor<File>() {
  216. @Override
  217. public boolean process(File file) {
  218. if (!file.isDirectory()) {
  219. String relativePath = FileUtil.getRelativePath(myRefsHeadsDir, file);
  220. if (relativePath != null) {
  221. branches.put(FileUtil.toSystemIndependentName(relativePath), file);
  222. }
  223. }
  224. return true;
  225. }
  226. });
  227. return branches;
  228. }
  229. /**
  230. * @return all branches in this repository. local/remote/active information is stored in branch objects themselves.
  231. */
  232. GitBranchesCollection readBranches() {
  233. Set<GitBranch> localBranches = readUnpackedLocalBranches();
  234. Set<GitBranch> remoteBranches = readUnpackedRemoteBranches();
  235. GitBranchesCollection packedBranches = readPackedBranches();
  236. localBranches.addAll(packedBranches.getLocalBranches());
  237. remoteBranches.addAll(packedBranches.getRemoteBranches());
  238. // note that even the active branch may be packed. So at first we collect branches, then we find the active.
  239. GitBranch currentBranch = readCurrentBranch();
  240. markActiveBranch(localBranches, currentBranch);
  241. return new GitBranchesCollection(localBranches, remoteBranches);
  242. }
  243. /**
  244. * Sets the 'active' flag to the current branch if it is contained in the specified collection.
  245. * @param branches branches to be walked through.
  246. * @param currentBranch current branch.
  247. */
  248. private static void markActiveBranch(@NotNull Set<GitBranch> branches, @Nullable GitBranch currentBranch) {
  249. if (currentBranch == null) {
  250. return;
  251. }
  252. for (GitBranch branch : branches) {
  253. if (branch.getName().equals(currentBranch.getName())) {
  254. branch.setActive(true);
  255. }
  256. }
  257. }
  258. /**
  259. * @return list of branches from refs/heads. active branch is not marked as active - the caller should do this.
  260. */
  261. @NotNull
  262. private Set<GitBranch> readUnpackedLocalBranches() {
  263. Set<GitBranch> branches = new HashSet<GitBranch>();
  264. for (Map.Entry<String, File> entry : readLocalBranches().entrySet()) {
  265. String branchName = entry.getKey();
  266. File branchFile = entry.getValue();
  267. String hash = loadHashFromBranchFile(branchFile);
  268. branches.add(new GitBranch(branchName, hash == null ? "" : hash, false, false));
  269. }
  270. return branches;
  271. }
  272. @Nullable
  273. private static String loadHashFromBranchFile(@NotNull File branchFile) {
  274. try {
  275. return tryLoadFile(branchFile, null);
  276. }
  277. catch (GitRepoStateException e) { // notify about error but don't break the process
  278. LOG.error("Couldn't read " + branchFile, e);
  279. }
  280. return null;
  281. }
  282. /**
  283. * @return list of branches from refs/remotes.
  284. */
  285. private Set<GitBranch> readUnpackedRemoteBranches() {
  286. final Set<GitBranch> branches = new HashSet<GitBranch>();
  287. if (!myRefsRemotesDir.exists()) {
  288. return branches;
  289. }
  290. FileUtil.processFilesRecursively(myRefsRemotesDir, new Processor<File>() {
  291. @Override
  292. public boolean process(File file) {
  293. if (!file.isDirectory()) {
  294. final String relativePath = FileUtil.getRelativePath(myRefsRemotesDir, file);
  295. if (relativePath != null) {
  296. String branchName = FileUtil.toSystemIndependentName(relativePath);
  297. String hash = loadHashFromBranchFile(file);
  298. branches.add(new GitBranch(branchName, hash == null ? "": hash, false, true));
  299. }
  300. }
  301. return true;
  302. }
  303. });
  304. return branches;
  305. }
  306. /**
  307. * @return list of local and remote branches from packed-refs. Active branch is not marked as active.
  308. */
  309. @NotNull
  310. private GitBranchesCollection readPackedBranches() {
  311. final Set<GitBranch> localBranches = new HashSet<GitBranch>();
  312. final Set<GitBranch> remoteBranches = new HashSet<GitBranch>();
  313. if (!myPackedRefsFile.exists()) {
  314. return GitBranchesCollection.EMPTY;
  315. }
  316. final String content = tryLoadFile(myPackedRefsFile, calcEncoding(myPackedRefsFile));
  317. for (String line : content.split("\n")) {
  318. parsePackedRefsLine(line, new PackedRefsLineResultHandler() {
  319. @Override public void handleResult(@Nullable String hash, @Nullable String branchName) {
  320. if (hash == null || branchName == null) {
  321. return;
  322. }
  323. if (branchName.startsWith(REFS_HEADS_PREFIX)) {
  324. localBranches.add(new GitBranch(branchName.substring(REFS_HEADS_PREFIX.length()), hash, false, false));
  325. } else if (branchName.startsWith(REFS_REMOTES_PREFIX)) {
  326. remoteBranches.add(new GitBranch(branchName.substring(REFS_REMOTES_PREFIX.length()), hash, false, true));
  327. }
  328. }
  329. });
  330. }
  331. return new GitBranchesCollection(localBranches, remoteBranches);
  332. }
  333. private static String readBranchFile(File branchFile) {
  334. String rev = tryLoadFile(branchFile, null); // we expect just hash in branch file, no need to check encoding
  335. return rev.trim();
  336. }
  337. private static void assertFileExists(File file, String message) {
  338. if (!file.exists()) {
  339. throw new GitRepoStateException(message);
  340. }
  341. }
  342. private Head readHead() {
  343. String headContent = tryLoadFile(myHeadFile, calcEncoding(myHeadFile));
  344. headContent = headContent.trim(); // remove possible leading and trailing spaces to clearly match regexps
  345. Matcher matcher = BRANCH_PATTERN.matcher(headContent);
  346. if (matcher.matches()) {
  347. return new Head(true, matcher.group(1));
  348. }
  349. if (COMMIT_PATTERN.matcher(headContent).matches()) {
  350. return new Head(false, headContent);
  351. }
  352. matcher = BRANCH_WEAK_PATTERN.matcher(headContent);
  353. if (matcher.matches()) {
  354. LOG.info(".git/HEAD has not standard format: [" + headContent + "]. We've parsed branch [" + matcher.group(1) + "]");
  355. return new Head(true, matcher.group(1));
  356. }
  357. throw new GitRepoStateException("Invalid format of the .git/HEAD file: \n" + headContent);
  358. }
  359. /**
  360. * Loads the file content.
  361. * Tries 3 times, then a {@link GitRepoStateException} is thrown.
  362. * @param file File to read.
  363. * @param encoding Encoding of the file, or null for using "UTF-8". Encoding is important for non-latin branch names.
  364. * @return file content.
  365. */
  366. @NotNull
  367. private static String tryLoadFile(@NotNull final File file, @Nullable final Charset encoding) {
  368. return tryOrThrow(new Callable<String>() {
  369. @Override
  370. public String call() throws Exception {
  371. return FileUtil.loadFile(file, encoding == null ? "UTF-8" : encoding.name());
  372. }
  373. }, file);
  374. }
  375. /**
  376. * Tries to execute the given action.
  377. * If an IOException happens, tries again up to 3 times, and then throws a {@link GitRepoStateException}.
  378. * If an other exception happens, rethrows it as a {@link GitRepoStateException}.
  379. * In the case of success returns the result of the task execution.
  380. */
  381. private static String tryOrThrow(Callable<String> actionToTry, File fileToLoad) {
  382. IOException cause = null;
  383. for (int i = 0; i < IO_RETRIES; i++) {
  384. try {
  385. return actionToTry.call();
  386. } catch (IOException e) {
  387. LOG.info("IOException while loading " + fileToLoad, e);
  388. cause = e;
  389. } catch (Exception e) { // this shouldn't happen since only IOExceptions are thrown in clients.
  390. throw new GitRepoStateException("Couldn't load file " + fileToLoad, e);
  391. }
  392. }
  393. throw new GitRepoStateException("Couldn't load file " + fileToLoad, cause);
  394. }
  395. /**
  396. * Parses a line from the .git/packed-refs file.
  397. * Passes the parsed hash-branch pair to the resultHandler.
  398. * Comments, tags and incorrectly formatted lines are ignored, and (null, null) is passed to the handler then.
  399. * Using a special handler may seem to be an overhead, but it is to avoid code duplication in two methods that parse packed-refs.
  400. */
  401. private static void parsePackedRefsLine(String line, PackedRefsLineResultHandler resultHandler) {
  402. try {
  403. line = line.trim();
  404. char firstChar = line.isEmpty() ? 0 : line.charAt(0);
  405. if (firstChar == '#') { // ignoring comments
  406. return;
  407. }
  408. if (firstChar == '^') {
  409. // ignoring the hash which an annotated tag above points to
  410. return;
  411. }
  412. String hash = null;
  413. int i;
  414. for (i = 0; i < line.length(); i++) {
  415. char c = line.charAt(i);
  416. if (!Character.isLetterOrDigit(c)) {
  417. hash = line.substring(0, i);
  418. break;
  419. }
  420. }
  421. String branch = null;
  422. int start = i;
  423. if (hash != null && start < line.length() && line.charAt(start++) == ' ') {
  424. for (i = start; i < line.length(); i++) {
  425. char c = line.charAt(i);
  426. if (Character.isWhitespace(c)) {
  427. break;
  428. }
  429. }
  430. branch = line.substring(start, i);
  431. }
  432. if (hash != null && branch != null) {
  433. resultHandler.handleResult(hash, branch);
  434. }
  435. else {
  436. LOG.info("Ignoring invalid packed-refs line: [" + line + "]");
  437. }
  438. }
  439. finally {
  440. resultHandler.handleResult(null, null);
  441. }
  442. }
  443. private interface PackedRefsLineResultHandler {
  444. void handleResult(@Nullable String hash, @Nullable String branchName);
  445. }
  446. /**
  447. * @return File encoding or <code>null</code> if the encoding is unknown.
  448. */
  449. @Nullable
  450. private static Charset calcEncoding(File file) {
  451. VirtualFile vf = VcsUtil.getVirtualFile(file);
  452. return ApplicationManager.getApplication().isDisposed() ? null : EncodingManager.getInstance().getEncoding(vf, false);
  453. }
  454. /**
  455. * Container to hold two information items: current .git/HEAD value and is Git on branch.
  456. */
  457. private static class Head {
  458. private final String ref;
  459. private final boolean isBranch;
  460. Head(boolean branch, String ref) {
  461. isBranch = branch;
  462. this.ref = ref;
  463. }
  464. }
  465. }